izteamslots 1.5.4 → 1.6.0

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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # [1.6.0](https://github.com/izzzzzi/izTeamSlots/compare/v1.5.5...v1.6.0) (2026-03-06)
2
+
3
+
4
+ ### Features
5
+
6
+ * **auth:** disable automatic admin login ([930313f](https://github.com/izzzzzi/izTeamSlots/commit/930313f6ef2f06674cc9da6e8d45061dc8c89a93))
7
+
8
+ ## [1.5.5](https://github.com/izzzzzi/izTeamSlots/compare/v1.5.4...v1.5.5) (2026-03-06)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **auth:** stabilize manual flows and tab focus ([1e909c2](https://github.com/izzzzzi/izTeamSlots/commit/1e909c220c3bf4120c5367e7e313632cc903a024))
14
+
1
15
  ## [1.5.4](https://github.com/izzzzzi/izTeamSlots/compare/v1.5.3...v1.5.4) (2026-03-06)
2
16
 
3
17
 
package/README.md CHANGED
@@ -94,6 +94,22 @@ npm cache clean --force && npm install -g izteamslots@latest
94
94
 
95
95
  > Chrome и chromedriver скачиваются автоматически при первом запуске через SeleniumBase.
96
96
 
97
+ ### Где лежат данные после `npm install -g`
98
+
99
+ При глобальной установке runtime-данные сохраняются не в папку пакета, а в пользовательскую директорию `~/.izteamslots`.
100
+
101
+ - `codex/`:
102
+ - Windows: `C:\Users\<USER>\.izteamslots\codex`
103
+ - macOS / Linux: `~/.izteamslots/codex`
104
+ - `.env`:
105
+ - Windows: `C:\Users\<USER>\.izteamslots\.env`
106
+ - macOS / Linux: `~/.izteamslots/.env`
107
+ - аккаунты и профили браузера:
108
+ - Windows: `C:\Users\<USER>\.izteamslots\accounts`
109
+ - macOS / Linux: `~/.izteamslots/accounts`
110
+
111
+ Если вы обновляете старую версию, `codex`-файлы могут временно лежать ещё и внутри директории пакета. В актуальной версии основным путём считается именно `~/.izteamslots`.
112
+
97
113
  ### Из исходников
98
114
 
99
115
  ```bash
@@ -237,4 +253,3 @@ flowchart TD
237
253
  - Проект хранит токены и браузерные профили локально на диске.
238
254
  - Не публикуйте папки `accounts/` и `codex/` в публичные репозитории.
239
255
  - Для стабильной работы перелогина у worker должен быть `openai_password`.
240
-
@@ -151,12 +151,17 @@ class Page:
151
151
  @property
152
152
  def url(self) -> str:
153
153
  try:
154
- return self._driver.current_url
154
+ current_url = self._driver.current_url
155
+ if current_url.startswith("chrome://") or current_url in {"about:blank", "data:,"}:
156
+ _activate_best_tab(self._driver)
157
+ current_url = self._driver.current_url
158
+ return current_url
155
159
  except Exception:
156
160
  return ""
157
161
 
158
162
  def goto(self, url: str, wait_until: str | None = None, timeout: int | None = None) -> None:
159
163
  self._driver.get(url)
164
+ _activate_best_tab(self._driver, [url, urllib.parse.urlparse(url).netloc])
160
165
  if wait_until in {"domcontentloaded", "load"}:
161
166
  t = (timeout or 30000) / 1000
162
167
  WebDriverWait(self._driver, t).until(
@@ -605,16 +610,18 @@ def _launch_page(profile_dir: Path, headless: bool = False) -> tuple[Page, Brows
605
610
  # "failed to close window in 20 seconds". Гарантируем наличие 2+ табов.
606
611
  try:
607
612
  handles = list(driver.window_handles)
613
+ primary_handle = driver.current_window_handle
608
614
  # Переключиться на основной (не chrome://) таб
609
615
  if len(handles) > 1:
610
616
  for h in handles:
611
617
  driver.switch_to.window(h)
612
618
  if not driver.current_url.startswith("chrome://"):
619
+ primary_handle = h
613
620
  break
614
621
  # Если только 1 таб — открыть второй пустой для UC mode
615
622
  if len(driver.window_handles) < 2:
616
623
  driver.execute_script("window.open('about:blank')")
617
- driver.switch_to.window(driver.window_handles[0])
624
+ driver.switch_to.window(primary_handle)
618
625
  except Exception:
619
626
  pass
620
627
 
@@ -673,6 +680,7 @@ def open_browser(
673
680
 
674
681
  _log(f"Открываю {url}...")
675
682
  driver.get(url)
683
+ _activate_best_tab(driver, [url, urllib.parse.urlparse(url).netloc])
676
684
 
677
685
  WebDriverWait(driver, 30).until(
678
686
  lambda d: d.execute_script("return document.readyState") in {"interactive", "complete"}
@@ -711,6 +719,42 @@ def _decode_jwt_payload(token: str) -> dict | None:
711
719
  return None
712
720
 
713
721
 
722
+ def _activate_best_tab(driver: Any, preferred_url_parts: list[str] | None = None) -> None:
723
+ preferred = [part for part in (preferred_url_parts or []) if part]
724
+
725
+ try:
726
+ handles = list(driver.window_handles)
727
+ except Exception:
728
+ return
729
+
730
+ best_handle: str | None = None
731
+ fallback_handle: str | None = None
732
+
733
+ for handle in handles:
734
+ try:
735
+ driver.switch_to.window(handle)
736
+ current_url = (driver.current_url or "").strip()
737
+ except Exception:
738
+ continue
739
+
740
+ if not current_url.startswith("chrome://") and fallback_handle is None:
741
+ fallback_handle = handle
742
+
743
+ if any(part in current_url for part in preferred):
744
+ best_handle = handle
745
+ break
746
+
747
+ if current_url and current_url not in {"about:blank", "data:,"}:
748
+ best_handle = handle
749
+
750
+ target = best_handle or fallback_handle
751
+ if target:
752
+ try:
753
+ driver.switch_to.window(target)
754
+ except Exception:
755
+ pass
756
+
757
+
714
758
  def _start_callback_server(state: str) -> tuple[HTTPServer, str, dict[str, str | None]]:
715
759
  holder: dict[str, str | None] = {"code": None, "error": None}
716
760
 
@@ -1132,24 +1176,26 @@ def oauth_login_manual(
1132
1176
 
1133
1177
  try:
1134
1178
  page.goto(authorize_url, wait_until="domcontentloaded", timeout=30000)
1179
+ _activate_best_tab(
1180
+ _context.driver,
1181
+ [
1182
+ "chatgpt.com",
1183
+ "auth.openai.com",
1184
+ "login_with",
1185
+ redirect_uri,
1186
+ ],
1187
+ )
1135
1188
  if expected_email:
1136
1189
  _log(f"В браузере завершите вход вручную для {expected_email}.")
1137
1190
  else:
1138
1191
  _log("В браузере завершите вход вручную.")
1139
- _log("Можно ввести email, пароль или код подтверждения вручную прямо в открытом окне.")
1140
- _log("После успешного входа дождусь OAuth callback и сохраню сессию автоматически.")
1192
+ _log("Я больше ничего не нажимаю: email, пароль, код и consent подтверждайте сами в браузере.")
1193
+ _log("Как только произойдёт переход на localhost callback, я перехвачу код и сохраню сессию.")
1141
1194
 
1142
- _handle_consent_and_wait(page, _log, callback_wait_seconds=timeout_seconds)
1143
1195
  auth_code = _wait_for_callback(holder, timeout=timeout_seconds)
1144
1196
  _log("Authorization code получен")
1145
1197
 
1146
1198
  session_result = _exchange_oauth_code(auth_code, redirect_uri, code_verifier)
1147
- _bootstrap_chatgpt_session(
1148
- page,
1149
- _log,
1150
- timeout_seconds=min(timeout_seconds, 300),
1151
- interactive=True,
1152
- )
1153
1199
  return page, session_result
1154
1200
  except Exception:
1155
1201
  try:
@@ -101,7 +101,13 @@ class SlotManager:
101
101
  raise RuntimeError("account_id/access_token не установлены — сначала login_admin()")
102
102
  return ChatGPTWorkspaceAPI(page, self.account_id, self.access_token)
103
103
 
104
- def _finalize_admin_login(self, page: Page, session: dict[str, Any]) -> None:
104
+ def _finalize_admin_login(
105
+ self,
106
+ page: Page,
107
+ session: dict[str, Any],
108
+ *,
109
+ manual_web_flow: bool = False,
110
+ ) -> None:
105
111
  if not self._admin:
106
112
  raise RuntimeError(f"Админ {self.admin_email} не найден в AccountStore")
107
113
 
@@ -115,38 +121,41 @@ class SlotManager:
115
121
  workspaces = get_workspaces(page, log=self._log)
116
122
  if workspaces:
117
123
  self._admin.workspaces = workspaces
118
- available_ids = {str(ws["workspace_id"]) for ws in workspaces if ws.get("workspace_id")}
119
124
  self._log(
120
125
  "Доступные workspace: "
121
126
  + ", ".join(f"{ws.get('name', '?')} [{ws.get('workspace_id', '?')}]" for ws in workspaces)
122
127
  )
123
128
 
124
- target_workspace_id = self._admin.workspace_id if self._admin.workspace_id in available_ids else None
125
- if self._admin.workspace_id and not target_workspace_id:
126
- self._log(
127
- f"[предупреждение] Workspace из OAuth ({self._admin.workspace_id}) не найден среди кнопок на странице"
128
- )
129
-
130
- if not target_workspace_id:
131
- for ws in workspaces:
132
- name = str(ws.get("name", ""))
133
- workspace_id = str(ws.get("workspace_id", ""))
134
- if workspace_id and "Личная" not in name and "Personal" not in name:
135
- target_workspace_id = workspace_id
136
- break
137
-
138
- if not target_workspace_id and workspaces:
139
- target_workspace_id = str(workspaces[0]["workspace_id"])
140
-
141
- self._admin.workspace_id = target_workspace_id
142
- if target_workspace_id:
143
- select_workspace(page, target_workspace_id, log=self._log)
144
- self._admin.account_id = target_workspace_id
145
- if not ensure_chatgpt_web_session(page, self._log, timeout_seconds=45, open_home=True):
146
- raise RuntimeError(
147
- "Не удалось подтвердить web-сессию chatgpt.com после выбора workspace"
129
+ if manual_web_flow:
130
+ self._log("Ручной режим: workspace не выбираю автоматически.")
131
+ else:
132
+ available_ids = {str(ws["workspace_id"]) for ws in workspaces if ws.get("workspace_id")}
133
+ target_workspace_id = self._admin.workspace_id if self._admin.workspace_id in available_ids else None
134
+ if self._admin.workspace_id and not target_workspace_id:
135
+ self._log(
136
+ f"[предупреждение] Workspace из OAuth ({self._admin.workspace_id}) не найден среди кнопок на странице"
148
137
  )
149
138
 
139
+ if not target_workspace_id:
140
+ for ws in workspaces:
141
+ name = str(ws.get("name", ""))
142
+ workspace_id = str(ws.get("workspace_id", ""))
143
+ if workspace_id and "Личная" not in name and "Personal" not in name:
144
+ target_workspace_id = workspace_id
145
+ break
146
+
147
+ if not target_workspace_id and workspaces:
148
+ target_workspace_id = str(workspaces[0]["workspace_id"])
149
+
150
+ self._admin.workspace_id = target_workspace_id
151
+ if target_workspace_id:
152
+ select_workspace(page, target_workspace_id, log=self._log)
153
+ self._admin.account_id = target_workspace_id
154
+ if not ensure_chatgpt_web_session(page, self._log, timeout_seconds=45, open_home=True):
155
+ raise RuntimeError(
156
+ "Не удалось подтвердить web-сессию chatgpt.com после выбора workspace"
157
+ )
158
+
150
159
  close_browser(page, log=self._log)
151
160
  self._admin.last_login = datetime.now(timezone.utc).isoformat()
152
161
  self.store.update_admin(self._admin)
@@ -161,28 +170,10 @@ class SlotManager:
161
170
  self._log("Админ авторизован!")
162
171
 
163
172
  def finalize_admin_session(self, page: Page, session: dict[str, Any]) -> None:
164
- self._finalize_admin_login(page, session)
173
+ self._finalize_admin_login(page, session, manual_web_flow=True)
165
174
 
166
175
  def login_admin(self) -> None:
167
- """Логин админа через OAuth PKCE получаем все токены (access, id, refresh)."""
168
- if not self._admin:
169
- raise RuntimeError(f"Админ {self.admin_email} не найден в AccountStore")
170
-
171
- mailbox = Mailbox(email=self._admin.email, password=self._admin.password)
172
- profile_dir = self.store.get_admin_profile_dir(self._admin)
173
- mail = self._get_admin_mail()
174
-
175
- self._log("Логин админа (OAuth PKCE)...")
176
- page, session = oauth_login(
177
- email=self._admin.email,
178
- password=self._admin.password,
179
- mail_client=mail,
180
- mailbox=mailbox,
181
- profile_dir=profile_dir,
182
- log=self._log,
183
- headless=self._headless,
184
- )
185
- self._finalize_admin_login(page, session)
176
+ raise RuntimeError("Авто-вход админа временно отключён. Используйте ручной вход через браузер.")
186
177
 
187
178
  def login_admin_manual(self) -> None:
188
179
  """Ручной логин админа в браузере с последующим автоматическим сохранением токенов."""
@@ -198,7 +189,7 @@ class SlotManager:
198
189
  )
199
190
  if session.get("email") and session["email"] != self._admin.email:
200
191
  self._log(f"[предупреждение] Вошли как {session['email']}, а не как {self._admin.email}")
201
- self._finalize_admin_login(page, session)
192
+ self._finalize_admin_login(page, session, manual_web_flow=True)
202
193
 
203
194
  def create_slots(self, count: int) -> list[WorkerAccount]:
204
195
  """Создать N временных почт."""
@@ -201,10 +201,7 @@ class UIFacade:
201
201
  self.manager = manager
202
202
 
203
203
  def login_admin(self, email: str, log: LogFunc) -> None:
204
- manager = SlotManager(store=self.store, admin_email=email, log=log)
205
- self._replace_manager(manager)
206
- manager.login_admin()
207
- self.sync_codex_files()
204
+ raise RuntimeError("Авто-вход админа временно отключён. Используйте ручной вход через браузер.")
208
205
 
209
206
  def login_admin_manual(self, email: str, log: LogFunc) -> None:
210
207
  manager = SlotManager(store=self.store, admin_email=email, log=log)
@@ -307,4 +304,3 @@ class UIFacade:
307
304
  log(f"Готово: {ok}/{total}")
308
305
  self.sync_codex_files()
309
306
  return {"ok": ok, "total": total}
310
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "izteamslots",
3
- "version": "1.5.4",
3
+ "version": "1.6.0",
4
4
  "description": "ChatGPT Team slot management — automated invite, register, OAuth login & codex token pipeline",
5
5
  "bin": {
6
6
  "izteamslots": "bin/izteamslots.mjs"