majorchik-api 0.0.0a0__py3-none-any.whl

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.
browser_ai/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from .gemini_api import GeminiAPI
2
+ from .deepseek_api import DeepSeekAPI
3
+ from .exceptions import ProfileNotFoundError
4
+
5
+ __all__ =["GeminiAPI", "DeepSeekAPI", "ProfileNotFoundError"]
browser_ai/base_api.py ADDED
@@ -0,0 +1,62 @@
1
+ from abc import ABC, abstractmethod
2
+ from playwright.sync_api import sync_playwright
3
+ import os
4
+ from .exceptions import ProfileNotFoundError
5
+
6
+ class BaseWebAPI(ABC):
7
+ """
8
+ Базовый класс для всех Web API моделей.
9
+ Инкапсулирует логику запуска браузера, маскировки и управления ресурсами.
10
+ """
11
+ def __init__(self, profile_dir: str, url: str, headless: bool = True, disable_images: bool = True, setup_mode: bool = False):
12
+ self.profile_dir = os.path.abspath(profile_dir)
13
+ self.url = url
14
+ self.headless = headless
15
+
16
+ if not setup_mode:
17
+ if not os.path.exists(self.profile_dir) or not os.listdir(self.profile_dir):
18
+ raise ProfileNotFoundError(f"Профиль не найден: {self.profile_dir}. Сначала создайте его через режим setup.")
19
+ else:
20
+ os.makedirs(self.profile_dir, exist_ok=True)
21
+
22
+ self.playwright = sync_playwright().start()
23
+
24
+ args =[
25
+ "--disable-blink-features=AutomationControlled",
26
+ "--no-sandbox",
27
+ "--disable-infobars"
28
+ ]
29
+
30
+ if disable_images:
31
+ args.append("--blink-settings=imagesEnabled=false")
32
+
33
+ self.browser = self.playwright.chromium.launch_persistent_context(
34
+ user_data_dir=self.profile_dir,
35
+ headless=headless,
36
+ channel="chrome",
37
+ ignore_default_args=["--enable-automation"],
38
+ args=args,
39
+ viewport={'width': 1280, 'height': 800},
40
+ permissions=["clipboard-read", "clipboard-write"]
41
+ )
42
+ self.page = self.browser.new_page()
43
+ self.page.add_init_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})")
44
+
45
+ self.page.goto(self.url, wait_until="domcontentloaded")
46
+
47
+ if not setup_mode:
48
+ self.wait_for_ready()
49
+
50
+ @abstractmethod
51
+ def wait_for_ready(self):
52
+ pass
53
+
54
+ @abstractmethod
55
+ def ask(self, prompt: str, files: list = None) -> str:
56
+ pass
57
+
58
+ def close(self):
59
+ if hasattr(self, 'browser') and self.browser:
60
+ self.browser.close()
61
+ if hasattr(self, 'playwright') and self.playwright:
62
+ self.playwright.stop()
browser_ai/cli.py ADDED
@@ -0,0 +1,93 @@
1
+ import sys
2
+ import argparse
3
+ import os
4
+ from .gemini_api import GeminiAPI
5
+ from .deepseek_api import DeepSeekAPI
6
+ from .exceptions import ProfileNotFoundError
7
+
8
+ def cmd_setup(args):
9
+ print(f"\n[Настройка профиля {args.model.upper()} | Директория: {args.profile_dir}]")
10
+ print("Открываем браузер...")
11
+
12
+ try:
13
+ if args.model == "gemini":
14
+ api = GeminiAPI(profile_dir=args.profile_dir, headless=False, setup_mode=True)
15
+ else:
16
+ api = DeepSeekAPI(profile_dir=args.profile_dir, headless=False, setup_mode=True)
17
+
18
+ print("\nПожалуйста, авторизуйтесь и пройдите все проверки (включая Cloudflare).")
19
+ print("Убедитесь, что интерфейс чата полностью загружен.")
20
+ input("\nНажми Enter в этой консоли для сохранения профиля и выхода...")
21
+
22
+ api.close()
23
+ print(f"Профиль {args.model} успешно сохранен в: {args.profile_dir}")
24
+ except Exception as e:
25
+ print(f"Ошибка при настройке: {e}")
26
+
27
+
28
+ def cmd_chat(args):
29
+ print(f"\nИнициализация {args.model.upper()} (профиль: {args.profile_dir}, headless: {args.headless})...")
30
+ ai = None
31
+ try:
32
+ if args.model == "gemini":
33
+ ai = GeminiAPI(profile_dir=args.profile_dir, headless=args.headless)
34
+ else:
35
+ ai = DeepSeekAPI(profile_dir=args.profile_dir, headless=args.headless)
36
+
37
+ print("Браузер готов. Вводите промпты (для выхода введите 'exit' или 'quit').")
38
+ print("-" * 50)
39
+
40
+ current_files = [os.path.abspath(f) for f in args.files] if args.files else None
41
+
42
+ while True:
43
+ prompt = input("\nПромпт: ").strip()
44
+ if prompt.lower() in ("exit", "quit"):
45
+ print("Завершение работы.")
46
+ break
47
+ if not prompt:
48
+ print("Промпт не может быть пустым.")
49
+ continue
50
+
51
+ print("\n[Ответ]:")
52
+ response = ai.ask(prompt, files=current_files)
53
+ print(response)
54
+ current_files = None
55
+
56
+ except ProfileNotFoundError as e:
57
+ print(f"\n[ОШИБКА] {e}")
58
+ print(f"Для создания профиля выполните команду:")
59
+ print(f" browser-ai setup {args.model} --profile-dir \"{args.profile_dir}\"")
60
+ except KeyboardInterrupt:
61
+ print("\n\nПрерывание пользователя.")
62
+ except Exception as e:
63
+ print(f"\nПроизошла ошибка: {e}")
64
+ finally:
65
+ if ai:
66
+ ai.close()
67
+
68
+
69
+ def main():
70
+ parser = argparse.ArgumentParser(description="Browser AI - Пакет для работы с Gemini и DeepSeek.")
71
+ subparsers = parser.add_subparsers(dest="command", required=True)
72
+
73
+ # Команда setup
74
+ parser_setup = subparsers.add_parser("setup", help="Создать и настроить профиль браузера (авторизация)")
75
+ parser_setup.add_argument("model", choices=["gemini", "deepseek"], help="Модель для настройки")
76
+ parser_setup.add_argument("--profile-dir", required=True, help="Путь к директории для сохранения профиля")
77
+
78
+ # Команда chat
79
+ parser_chat = subparsers.add_parser("chat", help="Запустить интерактивный чат с нейросетью")
80
+ parser_chat.add_argument("model", choices=["gemini", "deepseek"], help="Модель для использования")
81
+ parser_chat.add_argument("--profile-dir", required=True, help="Путь к директории сохраненного профиля")
82
+ parser_chat.add_argument("--headless", action="store_true", help="Скрыть визуально браузер (тихий режим)")
83
+ parser_chat.add_argument("--files", nargs="*", help="Пути к файлам для прикрепления к первому промпту")
84
+
85
+ args = parser.parse_args()
86
+
87
+ if args.command == "setup":
88
+ cmd_setup(args)
89
+ elif args.command == "chat":
90
+ cmd_chat(args)
91
+
92
+ if __name__ == "__main__":
93
+ main()
@@ -0,0 +1,141 @@
1
+ from .base_api import BaseWebAPI
2
+ import time
3
+ import re
4
+
5
+ def _clean_text(text: str) -> str:
6
+ return re.sub(r'\n{3,}', '\n\n', text).strip()
7
+
8
+ class DeepSeekAPI(BaseWebAPI):
9
+ def __init__(self, profile_dir: str, headless: bool = True, setup_mode: bool = False):
10
+ super().__init__(
11
+ profile_dir=profile_dir,
12
+ url="https://chat.deepseek.com/",
13
+ headless=headless,
14
+ disable_images=False,
15
+ setup_mode=setup_mode
16
+ )
17
+
18
+ def wait_for_ready(self):
19
+ self._handle_cloudflare()
20
+ self.page.wait_for_selector("textarea, #chat-input", timeout=30000)
21
+
22
+ def _handle_cloudflare(self):
23
+ time.sleep(3)
24
+ try:
25
+ for frame in self.page.frames:
26
+ if "cloudflare" in frame.url or "turnstile" in frame.url:
27
+ print("Обнаружен Cloudflare, пытаюсь пройти проверку...")
28
+ box = frame.locator("input[type='checkbox'], .cb-c, body")
29
+ if box.count() > 0:
30
+ box.first.click()
31
+ time.sleep(5)
32
+ except Exception as e:
33
+ print(f"Ошибка при прохождении Cloudflare: {e}")
34
+
35
+ def ask(self, prompt: str, files: list = None) -> str:
36
+ self.page.evaluate("navigator.clipboard.writeText('').catch(() => {})")
37
+
38
+ if files:
39
+ try:
40
+ file_input = self.page.locator('input[type="file"]')
41
+ file_input.set_input_files(files)
42
+ time.sleep(1.5)
43
+ loading_texts =["обработка", "Uploading", "Parsing", "processing", "В ожидании", "Загрузка"]
44
+ for text in loading_texts:
45
+ try:
46
+ for el in self.page.get_by_text(text).all():
47
+ if el.is_visible():
48
+ el.wait_for(state="hidden", timeout=30000)
49
+ except:
50
+ pass
51
+ time.sleep(2)
52
+ except Exception as e:
53
+ print(f"Не удалось прикрепить файлы: {e}")
54
+
55
+ md_selector = ".ds-markdown, .markdown-body, div[class*='markdown'], div[class*='prose']"
56
+
57
+ try:
58
+ initial_md_count = self.page.locator(md_selector).count()
59
+ initial_texts = self.page.locator(md_selector).all_inner_texts()
60
+ initial_last_text = initial_texts[-1].strip() if initial_texts else ""
61
+ except:
62
+ initial_md_count = 0
63
+ initial_last_text = ""
64
+
65
+ chat_input = self.page.locator("textarea, #chat-input").first
66
+ chat_input.fill(prompt)
67
+ time.sleep(0.5)
68
+ chat_input.press("Enter")
69
+
70
+ last_text = ""
71
+ stable_count = 0
72
+ max_wait_sec = 120
73
+
74
+ js_find_and_click = """
75
+ () => {
76
+ const mds = document.querySelectorAll('.ds-markdown, .markdown-body, div[class*="markdown"], div[class*="prose"]');
77
+ if (mds.length === 0) return false;
78
+ const lastMd = mds[mds.length - 1];
79
+ let curr = lastMd;
80
+ for (let i = 0; i < 5; i++) {
81
+ if (!curr) break;
82
+ let sibling = curr.nextElementSibling;
83
+ while (sibling) {
84
+ const isInputBlock = (sibling.matches && sibling.matches('textarea, #chat-input, form, [contenteditable="true"]')) ||
85
+ (sibling.querySelector && sibling.querySelector('textarea, #chat-input, form, [contenteditable="true"]'));
86
+ if (isInputBlock) {
87
+ sibling = sibling.nextElementSibling;
88
+ continue;
89
+ }
90
+ const btns = sibling.querySelectorAll('div.ds-icon-button[role="button"]');
91
+ if (btns.length >= 2) {
92
+ btns[0].click();
93
+ return true;
94
+ }
95
+ sibling = sibling.nextElementSibling;
96
+ }
97
+ curr = curr.parentElement;
98
+ }
99
+ return false;
100
+ }
101
+ """
102
+
103
+ for _ in range(max_wait_sec):
104
+ time.sleep(1)
105
+ try:
106
+ current_md_count = self.page.locator(md_selector).count()
107
+ texts = self.page.locator(md_selector).all_inner_texts()
108
+ texts =[t.strip() for t in texts if t.strip()]
109
+ current_text = texts[-1] if texts else ""
110
+ except:
111
+ current_md_count = 0
112
+ current_text = ""
113
+
114
+ has_new_message = False
115
+ if current_md_count > initial_md_count:
116
+ has_new_message = True
117
+ elif current_text and current_text != initial_last_text:
118
+ has_new_message = True
119
+
120
+ if has_new_message:
121
+ clicked = self.page.evaluate(js_find_and_click)
122
+ if clicked:
123
+ time.sleep(0.5)
124
+ clip_val = self.page.evaluate("navigator.clipboard.readText().catch(() => '')")
125
+ if clip_val:
126
+ return clip_val
127
+
128
+ if current_text and current_text == last_text:
129
+ stable_count += 1
130
+ else:
131
+ stable_count = 0
132
+
133
+ last_text = current_text
134
+
135
+ if stable_count >= 15:
136
+ break
137
+
138
+ if last_text:
139
+ return _clean_text(last_text)
140
+ else:
141
+ return "Не удалось найти ответ в DOM. Возможно, изменилась верстка."
@@ -0,0 +1,3 @@
1
+ class ProfileNotFoundError(Exception):
2
+ """Возникает, когда указанная директория профиля не существует или пуста."""
3
+ pass
@@ -0,0 +1,116 @@
1
+ from .base_api import BaseWebAPI
2
+ import time
3
+ import re
4
+
5
+ def _clean_text(text: str) -> str:
6
+ return re.sub(r'\n{3,}', '\n\n', text).strip()
7
+
8
+ class GeminiAPI(BaseWebAPI):
9
+ def __init__(self, profile_dir: str, headless: bool = True, setup_mode: bool = False):
10
+ super().__init__(
11
+ profile_dir=profile_dir,
12
+ url="https://aistudio.google.com/app/prompts/new_chat",
13
+ headless=headless,
14
+ disable_images=True,
15
+ setup_mode=setup_mode
16
+ )
17
+
18
+ def wait_for_ready(self):
19
+ self.page.wait_for_selector("textarea, [contenteditable='true']", timeout=15000)
20
+
21
+ def ask(self, prompt: str, files: list = None) -> str:
22
+ self.page.evaluate("navigator.clipboard.writeText('').catch(() => {})")
23
+
24
+ if files:
25
+ try:
26
+ self.page.locator('[data-test-id="add-media-button"], button[aria-label*="Insert"]').first.click()
27
+ file_input = self.page.locator('input[type="file"]')
28
+ file_input.wait_for(state="attached", timeout=5000)
29
+ file_input.set_input_files(files)
30
+
31
+ time.sleep(1.5)
32
+ loading_texts =["Loading", "В ожидании", "Загрузка", "Uploading"]
33
+ for text in loading_texts:
34
+ try:
35
+ for el in self.page.get_by_text(text).all():
36
+ if el.is_visible():
37
+ el.wait_for(state="hidden", timeout=30000)
38
+ except:
39
+ pass
40
+ time.sleep(1)
41
+ except Exception as e:
42
+ print(f"Не удалось прикрепить файлы: {e}")
43
+
44
+ try:
45
+ initial_thumb_count = self.page.locator('button[aria-label="Good response"], .turn-footer button, button:has(.material-symbols-outlined:has-text("thumb_up"))').count()
46
+ except:
47
+ initial_thumb_count = 0
48
+
49
+ self.page.keyboard.press("Escape")
50
+ self.page.wait_for_timeout(300)
51
+
52
+ chat_input = self.page.locator("textarea,[contenteditable='true'],[role='textbox']").first
53
+ chat_input.click(force=True)
54
+ chat_input.fill(prompt)
55
+ self.page.wait_for_timeout(500)
56
+
57
+ chat_input.press("Control+Enter")
58
+
59
+ max_wait_sec = 120
60
+ start_time = time.time()
61
+ completed = False
62
+
63
+ while time.time() - start_time < max_wait_sec:
64
+ try:
65
+ current_thumb_count = self.page.locator('button[aria-label="Good response"], .turn-footer button, button:has(.material-symbols-outlined:has-text("thumb_up"))').count()
66
+ if current_thumb_count > initial_thumb_count:
67
+ completed = True
68
+ break
69
+ except:
70
+ pass
71
+ self.page.wait_for_timeout(1000)
72
+
73
+ if not completed:
74
+ return "Не удалось дождаться завершения генерации (кнопка thumb_up не появилась)."
75
+
76
+ self.page.wait_for_timeout(1000)
77
+
78
+ try:
79
+ last_turn = self.page.locator('ms-chat-turn, div[data-turn-role="Model"], div.chat-turn-container.model').last
80
+
81
+ copy_success = False
82
+ try:
83
+ options_btn = last_turn.locator('button[aria-label="Open options"], button:has(.material-symbols-outlined:has-text("more_vert"))').last
84
+ if options_btn.count() > 0 and options_btn.is_visible():
85
+ options_btn.click(timeout=3000, force=True)
86
+ self.page.wait_for_timeout(500)
87
+
88
+ copy_btn = self.page.locator('button[role="menuitem"]:has-text("Copy as markdown"), button[role="menuitem"]:has-text("Copy")').first
89
+ if copy_btn.count() > 0 and copy_btn.is_visible():
90
+ copy_btn.click(timeout=3000, force=True)
91
+ self.page.wait_for_timeout(1000)
92
+ copy_success = True
93
+ except:
94
+ pass
95
+
96
+ if copy_success:
97
+ clip_val = self.page.evaluate("navigator.clipboard.readText().catch(() => '')")
98
+ if clip_val:
99
+ return clip_val
100
+
101
+ try:
102
+ last_turn.evaluate("""(el) => {
103
+ const footers = el.querySelectorAll('.turn-footer, .actions-container');
104
+ footers.forEach(f => f.style.display = 'none');
105
+ }""")
106
+ except:
107
+ pass
108
+
109
+ text = last_turn.inner_text()
110
+ if text.strip():
111
+ return _clean_text(text)
112
+
113
+ except Exception as e:
114
+ return f"Ошибка при извлечении текста: {e}"
115
+
116
+ return "Не удалось получить текст ответа."
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.4
2
+ Name: majorchik-api
3
+ Version: 0.0.0a0
4
+ Summary: Пакет для взаимодействия с современными нейросетями (Gemini, DeepSeek) через автоматизацию браузера
5
+ Requires-Python: >=3.8
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: playwright>=1.30.0
8
+
9
+ # Browser AI API
10
+
11
+ Инструмент для работы с Gemini и DeepSeek через браузерную автоматизацию (Playwright).
12
+ Поддерживает как консольное использование, так и работу в качестве Python-библиотеки.
@@ -0,0 +1,11 @@
1
+ browser_ai/__init__.py,sha256=Ijc8PmoI0UVuFz2rl4Z-ktylWWEgyxgqP3FNEnbetRM,185
2
+ browser_ai/base_api.py,sha256=9VGVdCTAARePLUnHLkb8956TE8qrM8o9A3_RLlNebJs,2435
3
+ browser_ai/cli.py,sha256=qQp-d40JazvOEfE0uRwgYniBm-m5QlRlYSBYiqT6iGQ,4462
4
+ browser_ai/deepseek_api.py,sha256=DjPq8xu2qeNCJ40J1nAJo69z_YRP6EyRKlHWX8eV5TI,5806
5
+ browser_ai/exceptions.py,sha256=7mgtuXSxLKwOsVtMLT_s4JinuDTN4VpDxCKoUE5VPug,192
6
+ browser_ai/gemini_api.py,sha256=_8y1ygKr8dLwP8lU8ENvhB3hsCgJ1V8ia2M8g--cv3A,5008
7
+ majorchik_api-0.0.0a0.dist-info/METADATA,sha256=E3EFr-GjvVRdrPo7tAC8DzpQ6PZCremABBLcUd0emhs,657
8
+ majorchik_api-0.0.0a0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
9
+ majorchik_api-0.0.0a0.dist-info/entry_points.txt,sha256=72XHK1czTPYqgdlXssWXnAuIfzMy02EorsTx7mgNJJY,51
10
+ majorchik_api-0.0.0a0.dist-info/top_level.txt,sha256=7OVRfN-g4pELdYb-THSCJyaDRJDatlImuU2Ud0HoceE,11
11
+ majorchik_api-0.0.0a0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ browser-ai = browser_ai.cli:main
@@ -0,0 +1 @@
1
+ browser_ai