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 +5 -0
- browser_ai/base_api.py +62 -0
- browser_ai/cli.py +93 -0
- browser_ai/deepseek_api.py +141 -0
- browser_ai/exceptions.py +3 -0
- browser_ai/gemini_api.py +116 -0
- majorchik_api-0.0.0a0.dist-info/METADATA +12 -0
- majorchik_api-0.0.0a0.dist-info/RECORD +11 -0
- majorchik_api-0.0.0a0.dist-info/WHEEL +5 -0
- majorchik_api-0.0.0a0.dist-info/entry_points.txt +2 -0
- majorchik_api-0.0.0a0.dist-info/top_level.txt +1 -0
browser_ai/__init__.py
ADDED
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. Возможно, изменилась верстка."
|
browser_ai/exceptions.py
ADDED
browser_ai/gemini_api.py
ADDED
|
@@ -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 @@
|
|
|
1
|
+
browser_ai
|