Перейти к основному содержимому

Как массово установить расширения в профили в Indigo X

Это руководство предоставляет пошаговую инструкцию по использованию мощного Python-скрипта для автоматизации установки ваших любимых расширений браузера в несколько профилей Indigo X.

Как это работает

Python-скрипт интеллектуально взаимодействует с локальным API Indigo X для:

  1. 🔐 Безопасной аутентификации: Вход в ваш аккаунт Indigo X для начала сессии.
  2. 💾 Кэширования токенов сессии: Сохранение токена автоматизации для минимизации запросов на вход и ускорения последующих запусков.
  3. 🚀 Создания или обновления профилей: В зависимости от вашей конфигурации, скрипт может:
    • Создать новые профили с предустановленными указанными расширениями.
    • Найти существующие профили и обновить их, добавив указанные расширения.
  4. Обработки лимитов API: Управление ограничениями API с автоматическим механизмом повторных попыток для обеспечения стабильности.

Необходимые условия

Перед началом убедитесь, что у вас есть следующее:

  • Python 3.6+: Если у вас нет Python, скачайте его с официального сайта python.org.

  • Requests библиотеки: Скрипт использует эту библиотеку для связи с API Indigo X. Установите его, открыв командную строку или терминал и выполнив эту команду:

    pip install requests
  • Аккаунт Indigo X: Активный аккаунт с запущенным приложением Indigo X.

  • Файлы расширений (распакованные): Скрипту нужны исходные файлы ваших расширений. Процесс немного отличается для профилей на основе Chromium и Firefox.

    • Для профилей Mimic (на основе Chromium): Вам нужна распакованная папка расширения.

      1. Используйте инструмент "CRX Extractor/Downloader" для загрузки расширения из Chrome Web Store в виде файла .zip.
      2. Извлеките содержимое .zip в отдельную папку. Скрипту потребуется полный путь к этой папке.
    • Для профилей Stealthfox (на основе Firefox): Вам нужен файл .xpi расширения.

      1. Перейдите на страницу дополнений Firefox для нужного расширения.
      2. Нажмите правой кнопкой на кнопку "Добавить в Firefox".
      3. Выберите "Сохранить ссылку как..." для загрузки расширения в виде .xpi. Скрипту потребуется полный путь к этому файлу.

Шаг 1: Python-скрипт

Сохраните следующий Python-код как indigo_extension_manager.py в новой папке на вашем компьютере. Этот скрипт является движком нашего процесса автоматизации.

Нажмите, чтобы просмотреть скрипт indigo_extension_manager.py
indigo_extension_manager.py
import hashlib
import json
import os
import sys
import time

import requests

# --- Константы ---
API_BASE_URL = "https://api.indigobrowser.com"
CONFIG_FILE_NAME = "config.json"
TOKEN_LIFETIME_SECONDS = 23 * 60 * 60 # 23 часа для 24-часового токена, для безопасности


def load_and_validate_config():
if not os.path.exists(CONFIG_FILE_NAME):
sys.exit(f"Ошибка: Файл конфигурации '{CONFIG_FILE_NAME}' не найден.")
with open(CONFIG_FILE_NAME, 'r') as f:
try:
config = json.load(f)
except json.JSONDecodeError:
sys.exit(f"Ошибка: Не удалось разобрать '{CONFIG_FILE_NAME}'.")

# Обязательные ключи
required_keys = ["email", "password", "workspace_name", "extension_paths", "action"]
missing_keys = [key for key in required_keys if key not in config]
if missing_keys:
sys.exit(f"Ошибка: В конфигурации отсутствуют обязательные ключи: {', '.join(missing_keys)}")

action = config.get("action")
if action == "create":
if "create_new_profiles_config" not in config:
sys.exit("Ошибка: Раздел 'create_new_profiles_config' отсутствует для действия 'create'.")
required_action_keys = ["base_name", "count", "folder_name", "os_type", "browser_type"]
cfg_section = config["create_new_profiles_config"]
section_name = "create_new_profiles_config"
elif action == "update":
if "update_existing_profiles_config" not in config:
sys.exit("Ошибка: Раздел 'update_existing_profiles_config' отсутствует для действия 'update'.")
required_action_keys = ["selection_method"]
cfg_section = config["update_existing_profiles_config"]
section_name = "update_existing_profiles_config"
if cfg_section.get("selection_method") == "by_folder" and "folder_name" not in cfg_section:
sys.exit(f"Ошибка: В разделе '{section_name}' отсутствует 'folder_name' для метода 'by_folder'.")
elif cfg_section.get("selection_method") == "by_name_search" and "search_text" not in cfg_section:
sys.exit(f"Ошибка: В разделе '{section_name}' отсутствует 'search_text' для метода 'by_name_search'.")
else:
sys.exit(f"Ошибка: Неверное действие '{action}' в конфигурации. Должно быть 'create' или 'update'.")

missing_action_keys = [key for key in required_action_keys if key not in cfg_section]
if missing_action_keys:
sys.exit(f"Ошибка: В разделе '{section_name}' отсутствуют ключи: {', '.join(missing_action_keys)}")

print("Конфигурация успешно загружена и проверена.")
return config


def save_token_to_config(config_path, token, expiration_timestamp, workspace_id):
config_data_to_save = {}
if os.path.exists(config_path):
with open(config_path, 'r') as f:
try:
config_data_to_save = json.load(f)
except json.JSONDecodeError:
print(f"Предупреждение: Не удалось прочитать существующий файл конфигурации ('{config_path}') для сохранения токена.")

config_data_to_save["cached_automation_token"] = token
config_data_to_save["token_expiration_timestamp"] = expiration_timestamp
config_data_to_save["workspace_id"] = workspace_id

with open(config_path, 'w') as f:
json.dump(config_data_to_save, f, indent=4)
print("Новый токен автоматизации, срок действия и ID рабочего пространства сохранены в config.json.")


def get_valid_cached_token(config):
cached_token = config.get("cached_automation_token")
expiration_ts = config.get("token_expiration_timestamp")
workspace_id = config.get("workspace_id")

if cached_token and isinstance(expiration_ts, (int, float)) and workspace_id:
if time.time() < expiration_ts:
# Базовая проверка: Попытка легковесного вызова API для проверки активности токена
try:
print("Проверка активности кэшированного токена...")
headers = {'Authorization': f'Bearer {cached_token}', 'Accept': 'application/json'}
verify_response = requests.get(f"{API_BASE_URL}/workspace/restrictions", headers=headers)
if verify_response.status_code == 200:
print("Найден действительный кэшированный токен автоматизации. Используем его.")
return cached_token, workspace_id
else:
print(
f"Проверка кэшированного токена не удалась (Статус: {verify_response.status_code}). Повторная аутентификация.")
return None, None
except requests.exceptions.RequestException as e:
print(f"Ошибка при проверке кэшированного токена: {e}. Повторная аутентификация.")
return None, None
else:
print("Срок действия кэшированного токена автоматизации истек.")
return None, None


def perform_full_login(config):
email, password, target_workspace_name = config['email'], config['password'], config['workspace_name']
hashed_password = hashlib.md5(password.encode()).hexdigest()
try:
print("Выполнение полного входа для получения нового токена автоматизации...")
print("Вход в систему...")
response = requests.post(f"{API_BASE_URL}/user/signin", json={'email': email, 'password': hashed_password})
response.raise_for_status()
data_payload = response.json().get('data', {})
initial_token, refresh_token = data_payload.get('token'), data_payload.get('refresh_token')
if not initial_token or not refresh_token:
print("Ошибка: Ответ на вход не содержит ожидаемых начального или refresh токенов.")
return None, None

print(f"Получение рабочих пространств для поиска '{target_workspace_name}'...")
headers = {'Authorization': f'Bearer {initial_token}', 'Accept': 'application/json'}
ws_response = requests.get(f"{API_BASE_URL}/user/workspaces", headers=headers)
ws_response.raise_for_status()
workspaces_list = ws_response.json().get('data', {}).get('workspaces', [])
target_workspace = next((ws for ws in workspaces_list if ws.get('name') == target_workspace_name), None)
if not target_workspace:
print(f"Ошибка: Не удалось найти рабочее пространство с именем '{target_workspace_name}'.")
return None, None
selected_workspace_id = target_workspace.get('workspace_id')
print(f"Найден ID рабочего пространства: {selected_workspace_id}")

print(f"Активация рабочего пространства...")
refresh_payload = {'email': email, 'refresh_token': refresh_token, 'workspace_id': selected_workspace_id}
refresh_response = requests.post(f"{API_BASE_URL}/user/refresh_token", headers=headers, json=refresh_payload)
refresh_response.raise_for_status()
refreshed_token = refresh_response.json().get('data', {}).get('token')
if not refreshed_token:
print("Ошибка: Не удалось получить новый токен для выбранного рабочего пространства.")
return None, None

print("Генерация токена автоматизации...")
headers['Authorization'] = f'Bearer {refreshed_token}'
auto_token_response = requests.get(f"{API_BASE_URL}/workspace/automation_token?expiration_period=24h",
headers=headers)
auto_token_response.raise_for_status()
automation_token = auto_token_response.json().get('data', {}).get('token')
if not automation_token:
print("Ошибка: В ответе не найден финальный токен автоматизации.")
return None, None

new_expiration_timestamp = time.time() + TOKEN_LIFETIME_SECONDS
save_token_to_config(CONFIG_FILE_NAME, automation_token, new_expiration_timestamp, selected_workspace_id)
print("\nНовый токен автоматизации успешно получен и сохранен в кэше!")
return automation_token, selected_workspace_id

except requests.exceptions.RequestException as e:
error_message = f"Ответ API: {e.response.text}" if hasattr(e,
'response') and e.response is not None else str(e)
print(f"Произошла ошибка при аутентификации: {error_message}")
return None, None
except Exception as e:
print(f"Произошла непредвиденная ошибка при аутентификации: {e}")
return None, None


def make_api_request(method, url, token, payload=None, max_retries=3, base_wait_time=5):
"""
Простая функция запроса с базовыми повторными попытками для ошибок 429.
Заменяет APIManager для простоты.
"""
headers = {
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
for attempt in range(max_retries):
try:
if method.upper() == 'POST':
response = requests.post(url, headers=headers, json=payload or {})
else: # GET
response = requests.get(url, headers=headers)

if response.status_code == 401: # Не авторизован, вероятно, истек срок токена
print(" -> Токен, похоже, недействителен или истек (401). Будет попытка обновить через основную логику.")
return None # Сигнал основной логике для повторной аутентификации

if response.status_code == 429:
wait_time = base_wait_time * (2 ** attempt)
print(f" -> Достигнут лимит запросов (429). Повторная попытка через {wait_time}с... (Попытка {attempt + 1}/{max_retries})")
time.sleep(wait_time)
continue

response.raise_for_status()
return response
except requests.exceptions.RequestException as e:
print(f" -> Произошла ошибка API: {e}")
# Если это клиентская ошибка, например 400, повторные попытки не помогут
if hasattr(e,
'response') and e.response is not None and 400 <= e.response.status_code < 500 and e.response.status_code != 429 and e.response.status_code != 401:
break
print(f" -> Запрос не удался после {max_retries} попыток для URL: {url}")
return None


def get_folder_id_by_name(config, token, folder_name):
print(f"\nПоиск папок для нахождения '{folder_name}'...")
try:
response = make_api_request('GET', f"{API_BASE_URL}/workspace/folders", token)
if response:
folders_list = response.json().get('data', {}).get('folders', [])
target_folder = next((f for f in folders_list if f.get('name') == folder_name), None)
if target_folder:
folder_id = target_folder.get('folder_id')
print(f"Найдена существующая папка с ID: {folder_id}")
return folder_id
except Exception as e:
print(f"Предупреждение: Не удалось получить существующие папки: {e}. Будет попытка создать.")
return None


def create_folder(config, token, folder_name):
print(f"Папка '{folder_name}' не найдена или ошибка при получении. Создаем ее сейчас...")
try:
create_payload = {"name": folder_name, "comment": ""}
response = make_api_request('POST', f"{API_BASE_URL}/workspace/folder_create", token, payload=create_payload)
if response:
new_folder_id = response.json().get('data', {}).get('id')
if new_folder_id:
print(f"Папка успешно создана с новым ID: {new_folder_id}")
return new_folder_id
except Exception as e:
print(f"Фатальная ошибка: Не удалось создать папку. {e}")
return None


def handle_new_profiles(config, token):
print("\n--- Выполнение действия: Создание новых профилей ---")
create_config = config['create_new_profiles_config']
folder_name = create_config['folder_name']

folder_id = get_folder_id_by_name(config, token, folder_name)
if not folder_id:
folder_id = create_folder(config, token, folder_name)
if not folder_id:
print("Фатальная ошибка: Папка не может быть найдена или создана. Прерывание создания профилей.")
return

comma_separated_paths = ",".join(config['extension_paths'])
cmd_params = {"params": [{"flag": "load-extension", "value": comma_separated_paths}]}

# Простая последовательная обработка с базовой задержкой
delay_between_requests = 2

for i in range(1, create_config['count'] + 1):
profile_name = f"{create_config['base_name']}{i}"
payload = {
"name": profile_name, "browser_type": create_config['browser_type'],
"os_type": create_config['os_type'], "folder_id": folder_id,
"parameters": {
"fingerprint": {"cmd_params": cmd_params},
"storage": {"is_local": False}
}
}
print(f"({i}/{create_config['count']}) Создание профиля '{profile_name}'...")
response = make_api_request('POST', f"{API_BASE_URL}/profile/create", token, payload=payload)
if response and response.status_code < 400:
print(f" -> Успех!")
else:
print(f" -> Не удалось обработать профиль '{profile_name}'.") # Детали ошибки из make_api_request

if i < create_config['count']:
time.sleep(delay_between_requests) # Простая задержка
print("\nВсе запросы на создание профилей обработаны.")


def handle_existing_profiles(config, token):
print("\n--- Выполнение действия: Обновление существующих профилей ---")
update_config = config['update_existing_profiles_config']
search_payload = {
"limit": 100, "offset": 0, "is_removed": False,
"storage_type": "all", "search_text": ""
}

selection_method = update_config.get("selection_method")
if selection_method == "by_folder":
folder_name_to_search = update_config.get("folder_name")
if not folder_name_to_search:
sys.exit("Ошибка: 'folder_name' отсутствует для метода 'by_folder'.")
folder_id = get_folder_id_by_name(config, token, folder_name_to_search)
if not folder_id:
print(f"Ошибка: Папка '{folder_name_to_search}' не найдена. Невозможно обновить профили.")
return
search_payload["folder_id"] = folder_id
elif selection_method == "by_name_search":
search_text_to_use = update_config.get("search_text")
if search_text_to_use is None:
sys.exit("Ошибка: 'search_text' отсутствует для метода 'by_name_search'.")
search_payload["search_text"] = search_text_to_use
else:
sys.exit(f"Ошибка: Неверный selection_method '{selection_method}'.")

try:
response = make_api_request('POST', f"{API_BASE_URL}/profile/search", token, payload=search_payload)
if not response: return
profiles_to_update = response.json().get('data', {}).get('profiles', [])
if not profiles_to_update:
print("Не найдено профилей, соответствующих вашим критериям.");
return
except Exception as e:
print(f"Ошибка при поиске профилей: {e}");
return

profile_names_found = [p.get('name', 'Безымянный профиль') for p in profiles_to_update]
print(f"Найдено {len(profiles_to_update)} профилей для обновления: {profile_names_found}")

comma_separated_paths = ",".join(config['extension_paths'])
cmd_params = {"params": [{"flag": "load-extension", "value": comma_separated_paths}]}

delay_between_requests = 2 # Простая фиксированная задержка

for idx, profile in enumerate(profiles_to_update):
profile_id = profile.get('id')
profile_name_to_update = profile.get('name', 'Безымянный профиль')
if not profile_id: print(f"Предупреждение: Пропуск профиля с отсутствующим ID: {profile}"); continue

print(f"({idx + 1}/{len(profiles_to_update)}) Обновление профиля '{profile_name_to_update}'...")
payload = {"profile_id": profile_id, "parameters": {"fingerprint": {"cmd_params": cmd_params}}}
response = make_api_request('POST', f"{API_BASE_URL}/profile/partial_update", token, payload=payload)

if response and response.status_code < 400:
print(f" -> Успех!")
else:
print(f" -> Не удалось обработать профиль '{profile_name_to_update}'.")

if idx < len(profiles_to_update) - 1:
time.sleep(delay_between_requests) # Простая задержка
print("\nВсе запросы на обновление профилей обработаны.")


def main():
config = load_and_validate_config()

automation_token, workspace_id = get_valid_cached_token(config)

if not automation_token:
print("Действительный кэшированный токен не найден или срок его действия истек. Производим полный вход.")
token_data = perform_full_login(config)
if not token_data or not token_data[0]:
sys.exit("Не удалось получить токен автоматизации. Выход.")
automation_token, workspace_id = token_data
# Функция perform_full_login теперь сохраняет токен и workspace_id в конфиг
else:
print("Используется кэшированный токен автоматизации.")

# Сохранение полученного/проверенного токена и workspace_id обратно в runtime config
config['api_token'] = automation_token
config['workspace_id'] = workspace_id

print("\n--- Аутентификация завершена ---")

action = config.get("action")
if action == "create":
handle_new_profiles(config, automation_token) # Передача токена напрямую
elif action == "update":
handle_existing_profiles(config, automation_token) # Передача токена напрямую


if __name__ == "__main__":
main()

Шаг 2: Подготовка файла config.json

В той же папке, что и ваш Python-скрипт, создайте файл с именем config.json. Этот файл указывает скрипту, что именно нужно сделать. Скопируйте и вставьте шаблон ниже, изменив его в соответствии с вашими данными.

Нажмите, чтобы просмотреть файл config.json
config.json
{
"email": "ваш[email protected]",
"password": "ВашПарольДляIndigoX",
"workspace_name": "Мое Основное Рабочее Пространство",
"action": "create",
"extension_paths": [
"/Users/ВашПользователь/indigo_extensions/ublock_unpacked",
"/Users/ВашПользователь/indigo_extensions/buster.xpi"
],
"create_new_profiles_config": {
"base_name": "Новый-Профиль-",
"count": 2,
"folder_name": "Автоматизированные Профили",
"os_type": "windows",
"browser_type": "mimic"
},
"update_existing_profiles_config": {
"selection_method": "by_folder",
"folder_name": "Профили Для Обновления",
"search_text": ""
},
"cached_automation_token": null,
"token_expiration_timestamp": null,
"workspace_id": null
}

Детали конфигурации

  • "email": Ваш email для входа в Indigo X.
  • "password": Ваш пароль для входа в Indigo X.
    • 🛡️ Важно для безопасности: Пароль хранится в открытом виде. Убедитесь, что этот файл хранится в безопасном месте и не передается или не загружается в публичные системы контроля версий.
  • "workspace_name": Точное email рабочего пространства Indigo X, которое вы хотите использовать, как оно отображается в приложении Indigo.
  • "action": Определяет операцию скрипта.
    • Установите "create" для создания новых профилей с расширениями.
    • Установите "update" для добавления расширений в существующие профили.
  • "extension_paths": Список строк. Каждая строка должна быть полным абсолютным путем к:
    • Папке с распакованным расширением Mimic (Chromium).
    • Файлу .xpi для расширения Stealthfox (Firefox).
  • "create_new_profiles_config": Используется только при "action" равном "create".
    • "base_name": Префикс для имен новых профилей (например, "Маркетинг-" создаст "Маркетинг-1", "Маркетинг-2").
    • "count": Количество новых профилей для создания.
    • "folder_name": Папка, в которой будут созданы новые профили (скрипт создаст ее, если она не существует). По умолчанию новые профили сохраняются в облаке (is_local: false).
    • "os_type": ОС для новых профилей ("windows", "macos", "linux", "android").
    • "browser_type": Браузер для новых профилей ("mimic", "stealthfox").
  • "update_existing_profiles_config": Используется только при "action" равном "update".
    • "selection_method": Как искать профили для обновления.
      • "by_folder": Выбирает все профили в указанной "folder_name".
      • "by_name_search": Выбирает все профили, соответствующие указанному "search_text".
    • "folder_name": Имя папки для выбора (если используется "by_folder").
    • "search_text": Текст для поиска в именах профилей (если используется "by_name_search").
  • "cached_automation_token", "token_expiration_timestamp", "workspace_id": Управляются скриптом автоматически. Изначально можно оставить их как null.