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 +14 -0
- package/README.md +16 -1
- package/backend/openai_web_auth.py +57 -11
- package/backend/slot_orchestrator.py +38 -47
- package/backend/ui_facade.py +1 -5
- package/package.json +1 -1
- package/ui/src/menus/format.ts +199 -86
- package/ui/src/menus/mainMenus.ts +19 -16
- package/ui/src/screens/MainScreen.ts +468 -116
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
|
-
|
|
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
|
-
|
|
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("
|
|
1140
|
-
_log("
|
|
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(
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
"
|
|
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 временных почт."""
|
package/backend/ui_facade.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|