izteamslots 1.1.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/.env.example +1 -0
- package/CONTRIBUTING.md +128 -0
- package/README.md +249 -0
- package/app.py +25 -0
- package/backend/__init__.py +3 -0
- package/backend/__main__.py +3 -0
- package/backend/account_store.py +448 -0
- package/backend/chatgpt_workspace_api.py +104 -0
- package/backend/dto.py +106 -0
- package/backend/file_logger.py +82 -0
- package/backend/jobs.py +77 -0
- package/backend/mail/__init__.py +98 -0
- package/backend/mail/base.py +86 -0
- package/backend/mail/boomlify.py +178 -0
- package/backend/mail/imap.py +221 -0
- package/backend/mail/trickads.py +121 -0
- package/backend/openai_web_auth.py +1402 -0
- package/backend/rpc_protocol.py +78 -0
- package/backend/rpc_server.py +233 -0
- package/backend/slot_orchestrator.py +400 -0
- package/backend/ui_facade.py +368 -0
- package/bin/izteamslots.sh +16 -0
- package/package.json +30 -0
- package/requirements.txt +2 -0
- package/scripts/setup.sh +82 -0
- package/ui/package.json +19 -0
- package/ui/src/main.ts +4 -0
- package/ui/src/menus/format.ts +163 -0
- package/ui/src/menus/mainMenus.ts +221 -0
- package/ui/src/menus/types.ts +75 -0
- package/ui/src/screens/MainScreen.ts +1175 -0
- package/ui/src/transport/stdioClient.ts +162 -0
- package/ui/tsconfig.json +13 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
import uuid
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from . import PROJECT_ROOT
|
|
12
|
+
|
|
13
|
+
ACCOUNTS_DIR = PROJECT_ROOT / "accounts"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class AdminAccount:
|
|
18
|
+
id: str
|
|
19
|
+
email: str
|
|
20
|
+
password: str
|
|
21
|
+
access_token: str | None = None
|
|
22
|
+
workspace_id: str | None = None
|
|
23
|
+
account_id: str | None = None
|
|
24
|
+
workspaces: list[dict] | None = None
|
|
25
|
+
created_at: str | None = None
|
|
26
|
+
last_login: str | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class WorkerAccount:
|
|
31
|
+
id: str
|
|
32
|
+
email: str
|
|
33
|
+
password: str
|
|
34
|
+
status: str = "created" # created -> invited -> registered -> logged_in
|
|
35
|
+
openai_password: str | None = None
|
|
36
|
+
access_token: str | None = None
|
|
37
|
+
workspace_id: str | None = None
|
|
38
|
+
admin_email: str | None = None
|
|
39
|
+
created_at: str | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class AccountStore:
|
|
43
|
+
"""Единый CRUD для admin и worker аккаунтов."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, base_dir: Path = ACCOUNTS_DIR) -> None:
|
|
46
|
+
self.base_dir = base_dir
|
|
47
|
+
self.admin_dir = base_dir / "admin"
|
|
48
|
+
self.worker_dir = base_dir / "worker"
|
|
49
|
+
self.admin_dir.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
self.worker_dir.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
|
|
52
|
+
# --- Helpers ---
|
|
53
|
+
|
|
54
|
+
def _read_index(self, path: Path) -> dict:
|
|
55
|
+
index_path = path / "index.json"
|
|
56
|
+
if index_path.exists():
|
|
57
|
+
return json.loads(index_path.read_text())
|
|
58
|
+
return {}
|
|
59
|
+
|
|
60
|
+
def _write_index(self, path: Path, data: dict) -> None:
|
|
61
|
+
index_path = path / "index.json"
|
|
62
|
+
index_path.write_text(json.dumps(data, indent=2, ensure_ascii=False))
|
|
63
|
+
|
|
64
|
+
def _read_meta(self, folder: Path) -> dict:
|
|
65
|
+
meta_path = folder / "meta.json"
|
|
66
|
+
if meta_path.exists():
|
|
67
|
+
return json.loads(meta_path.read_text())
|
|
68
|
+
return {}
|
|
69
|
+
|
|
70
|
+
def _write_meta(self, folder: Path, data: dict) -> None:
|
|
71
|
+
folder.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
meta_path = folder / "meta.json"
|
|
73
|
+
meta_path.write_text(json.dumps(data, indent=2, ensure_ascii=False))
|
|
74
|
+
|
|
75
|
+
# --- Admin CRUD ---
|
|
76
|
+
|
|
77
|
+
def list_admins(self) -> list[AdminAccount]:
|
|
78
|
+
index = self._read_index(self.admin_dir)
|
|
79
|
+
admins = []
|
|
80
|
+
for email, info in index.items():
|
|
81
|
+
account_dir = self.admin_dir / info["id"]
|
|
82
|
+
meta = self._read_meta(account_dir)
|
|
83
|
+
admins.append(AdminAccount(
|
|
84
|
+
id=info["id"],
|
|
85
|
+
email=email,
|
|
86
|
+
password=meta.get("password", ""),
|
|
87
|
+
access_token=meta.get("access_token"),
|
|
88
|
+
workspace_id=meta.get("workspace_id"),
|
|
89
|
+
account_id=meta.get("account_id"),
|
|
90
|
+
workspaces=meta.get("workspaces"),
|
|
91
|
+
created_at=info.get("created_at"),
|
|
92
|
+
last_login=info.get("last_login"),
|
|
93
|
+
))
|
|
94
|
+
return admins
|
|
95
|
+
|
|
96
|
+
def get_admin(self, email: str) -> AdminAccount | None:
|
|
97
|
+
index = self._read_index(self.admin_dir)
|
|
98
|
+
info = index.get(email)
|
|
99
|
+
if not info:
|
|
100
|
+
return None
|
|
101
|
+
account_dir = self.admin_dir / info["id"]
|
|
102
|
+
meta = self._read_meta(account_dir)
|
|
103
|
+
return AdminAccount(
|
|
104
|
+
id=info["id"],
|
|
105
|
+
email=email,
|
|
106
|
+
password=meta.get("password", ""),
|
|
107
|
+
access_token=meta.get("access_token"),
|
|
108
|
+
workspace_id=meta.get("workspace_id"),
|
|
109
|
+
account_id=meta.get("account_id"),
|
|
110
|
+
workspaces=meta.get("workspaces"),
|
|
111
|
+
created_at=info.get("created_at"),
|
|
112
|
+
last_login=info.get("last_login"),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def add_admin(self, email: str, password: str) -> AdminAccount:
|
|
116
|
+
index = self._read_index(self.admin_dir)
|
|
117
|
+
if email in index:
|
|
118
|
+
raise ValueError(f"Админ {email} уже существует")
|
|
119
|
+
account_id = uuid.uuid4().hex
|
|
120
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
121
|
+
index[email] = {"id": account_id, "created_at": now}
|
|
122
|
+
self._write_index(self.admin_dir, index)
|
|
123
|
+
account_dir = self.admin_dir / account_id
|
|
124
|
+
self._write_meta(account_dir, {"email": email, "password": password})
|
|
125
|
+
return AdminAccount(id=account_id, email=email, password=password, created_at=now)
|
|
126
|
+
|
|
127
|
+
def update_admin(self, account: AdminAccount) -> None:
|
|
128
|
+
index = self._read_index(self.admin_dir)
|
|
129
|
+
if account.email not in index:
|
|
130
|
+
raise ValueError(f"Админ {account.email} не найден")
|
|
131
|
+
info = index[account.email]
|
|
132
|
+
if account.last_login:
|
|
133
|
+
info["last_login"] = account.last_login
|
|
134
|
+
self._write_index(self.admin_dir, index)
|
|
135
|
+
account_dir = self.admin_dir / info["id"]
|
|
136
|
+
meta: dict[str, Any] = {
|
|
137
|
+
"email": account.email,
|
|
138
|
+
"password": account.password,
|
|
139
|
+
}
|
|
140
|
+
if account.access_token is not None:
|
|
141
|
+
meta["access_token"] = account.access_token
|
|
142
|
+
if account.workspace_id is not None:
|
|
143
|
+
meta["workspace_id"] = account.workspace_id
|
|
144
|
+
if account.account_id is not None:
|
|
145
|
+
meta["account_id"] = account.account_id
|
|
146
|
+
if account.workspaces is not None:
|
|
147
|
+
meta["workspaces"] = account.workspaces
|
|
148
|
+
self._write_meta(account_dir, meta)
|
|
149
|
+
|
|
150
|
+
def delete_admin(self, email: str) -> None:
|
|
151
|
+
index = self._read_index(self.admin_dir)
|
|
152
|
+
info = index.pop(email, None)
|
|
153
|
+
if not info:
|
|
154
|
+
return
|
|
155
|
+
self._write_index(self.admin_dir, index)
|
|
156
|
+
account_dir = self.admin_dir / info["id"]
|
|
157
|
+
if account_dir.exists():
|
|
158
|
+
shutil.rmtree(account_dir)
|
|
159
|
+
|
|
160
|
+
def get_admin_profile_dir(self, account: AdminAccount) -> Path:
|
|
161
|
+
profile_dir = self.admin_dir / account.id / "browser_profile"
|
|
162
|
+
profile_dir.mkdir(parents=True, exist_ok=True)
|
|
163
|
+
return profile_dir
|
|
164
|
+
|
|
165
|
+
# --- Worker CRUD ---
|
|
166
|
+
|
|
167
|
+
def list_workers(self) -> list[WorkerAccount]:
|
|
168
|
+
index = self._read_index(self.worker_dir)
|
|
169
|
+
workers = []
|
|
170
|
+
for email, info in index.items():
|
|
171
|
+
account_dir = self.worker_dir / info["id"]
|
|
172
|
+
meta = self._read_meta(account_dir)
|
|
173
|
+
workers.append(WorkerAccount(
|
|
174
|
+
id=info["id"],
|
|
175
|
+
email=email,
|
|
176
|
+
password=meta.get("password", ""),
|
|
177
|
+
status=meta.get("status", info.get("status", "created")),
|
|
178
|
+
openai_password=meta.get("openai_password"),
|
|
179
|
+
access_token=meta.get("access_token"),
|
|
180
|
+
workspace_id=meta.get("workspace_id"),
|
|
181
|
+
admin_email=info.get("admin_email"),
|
|
182
|
+
created_at=info.get("created_at"),
|
|
183
|
+
))
|
|
184
|
+
return workers
|
|
185
|
+
|
|
186
|
+
def get_worker(self, email: str) -> WorkerAccount | None:
|
|
187
|
+
index = self._read_index(self.worker_dir)
|
|
188
|
+
info = index.get(email)
|
|
189
|
+
if not info:
|
|
190
|
+
return None
|
|
191
|
+
account_dir = self.worker_dir / info["id"]
|
|
192
|
+
meta = self._read_meta(account_dir)
|
|
193
|
+
return WorkerAccount(
|
|
194
|
+
id=info["id"],
|
|
195
|
+
email=email,
|
|
196
|
+
password=meta.get("password", ""),
|
|
197
|
+
status=meta.get("status", info.get("status", "created")),
|
|
198
|
+
openai_password=meta.get("openai_password"),
|
|
199
|
+
access_token=meta.get("access_token"),
|
|
200
|
+
workspace_id=meta.get("workspace_id"),
|
|
201
|
+
admin_email=info.get("admin_email"),
|
|
202
|
+
created_at=info.get("created_at"),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def add_worker(self, email: str, password: str, admin_email: str) -> WorkerAccount:
|
|
206
|
+
index = self._read_index(self.worker_dir)
|
|
207
|
+
if email in index:
|
|
208
|
+
raise ValueError(f"Worker {email} уже существует")
|
|
209
|
+
worker_id = uuid.uuid4().hex
|
|
210
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
211
|
+
index[email] = {
|
|
212
|
+
"id": worker_id,
|
|
213
|
+
"status": "created",
|
|
214
|
+
"admin_email": admin_email,
|
|
215
|
+
"created_at": now,
|
|
216
|
+
}
|
|
217
|
+
self._write_index(self.worker_dir, index)
|
|
218
|
+
account_dir = self.worker_dir / worker_id
|
|
219
|
+
self._write_meta(account_dir, {
|
|
220
|
+
"email": email,
|
|
221
|
+
"password": password,
|
|
222
|
+
"status": "created",
|
|
223
|
+
})
|
|
224
|
+
return WorkerAccount(
|
|
225
|
+
id=worker_id, email=email, password=password,
|
|
226
|
+
admin_email=admin_email, created_at=now,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
def update_worker(self, account: WorkerAccount) -> None:
|
|
230
|
+
index = self._read_index(self.worker_dir)
|
|
231
|
+
if account.email not in index:
|
|
232
|
+
raise ValueError(f"Worker {account.email} не найден")
|
|
233
|
+
info = index[account.email]
|
|
234
|
+
info["status"] = account.status
|
|
235
|
+
if account.admin_email is not None:
|
|
236
|
+
info["admin_email"] = account.admin_email
|
|
237
|
+
else:
|
|
238
|
+
info.pop("admin_email", None)
|
|
239
|
+
self._write_index(self.worker_dir, index)
|
|
240
|
+
account_dir = self.worker_dir / info["id"]
|
|
241
|
+
meta: dict[str, Any] = {
|
|
242
|
+
"email": account.email,
|
|
243
|
+
"password": account.password,
|
|
244
|
+
"status": account.status,
|
|
245
|
+
}
|
|
246
|
+
if account.openai_password is not None:
|
|
247
|
+
meta["openai_password"] = account.openai_password
|
|
248
|
+
if account.access_token is not None:
|
|
249
|
+
meta["access_token"] = account.access_token
|
|
250
|
+
if account.workspace_id is not None:
|
|
251
|
+
meta["workspace_id"] = account.workspace_id
|
|
252
|
+
self._write_meta(account_dir, meta)
|
|
253
|
+
|
|
254
|
+
def delete_worker(self, email: str) -> None:
|
|
255
|
+
index = self._read_index(self.worker_dir)
|
|
256
|
+
info = index.pop(email, None)
|
|
257
|
+
if not info:
|
|
258
|
+
return
|
|
259
|
+
self._write_index(self.worker_dir, index)
|
|
260
|
+
account_dir = self.worker_dir / info["id"]
|
|
261
|
+
if account_dir.exists():
|
|
262
|
+
shutil.rmtree(account_dir)
|
|
263
|
+
|
|
264
|
+
def get_worker_profile_dir(self, account: WorkerAccount) -> Path:
|
|
265
|
+
profile_dir = self.worker_dir / account.id / "browser_profile"
|
|
266
|
+
profile_dir.mkdir(parents=True, exist_ok=True)
|
|
267
|
+
return profile_dir
|
|
268
|
+
|
|
269
|
+
# --- Doctor ---
|
|
270
|
+
|
|
271
|
+
def doctor(self) -> list[str]:
|
|
272
|
+
"""Проверяет целостность данных и исправляет проблемы.
|
|
273
|
+
Возвращает список выполненных исправлений."""
|
|
274
|
+
fixes: list[str] = []
|
|
275
|
+
|
|
276
|
+
for role, role_dir in [("admin", self.admin_dir), ("worker", self.worker_dir)]:
|
|
277
|
+
# 1. Проверяем index.json — читаемый ли
|
|
278
|
+
index_path = role_dir / "index.json"
|
|
279
|
+
if index_path.exists():
|
|
280
|
+
try:
|
|
281
|
+
index = json.loads(index_path.read_text())
|
|
282
|
+
except (json.JSONDecodeError, OSError):
|
|
283
|
+
# Битый index — пересобираем из папок
|
|
284
|
+
index = self._rebuild_index(role_dir, role)
|
|
285
|
+
fixes.append(f"{role}: index.json был повреждён — пересобран из папок")
|
|
286
|
+
else:
|
|
287
|
+
index = {}
|
|
288
|
+
|
|
289
|
+
changed = False
|
|
290
|
+
to_remove: list[str] = []
|
|
291
|
+
|
|
292
|
+
for email, info in list(index.items()):
|
|
293
|
+
account_id = info.get("id")
|
|
294
|
+
if not account_id:
|
|
295
|
+
to_remove.append(email)
|
|
296
|
+
fixes.append(f"{role}: {email} — нет id в index, запись удалена")
|
|
297
|
+
continue
|
|
298
|
+
|
|
299
|
+
account_dir = role_dir / account_id
|
|
300
|
+
|
|
301
|
+
# 2. Папка аккаунта не существует — создаём с meta.json
|
|
302
|
+
if not account_dir.exists():
|
|
303
|
+
account_dir.mkdir(parents=True, exist_ok=True)
|
|
304
|
+
meta = {"email": email}
|
|
305
|
+
if "password" not in info:
|
|
306
|
+
meta["password"] = ""
|
|
307
|
+
else:
|
|
308
|
+
meta["password"] = info["password"]
|
|
309
|
+
# Для worker берём данные из index
|
|
310
|
+
if role == "worker":
|
|
311
|
+
meta["status"] = info.get("status", "created")
|
|
312
|
+
self._write_meta(account_dir, meta)
|
|
313
|
+
fixes.append(f"{role}: {email} — папка {account_id} отсутствовала, создана заново")
|
|
314
|
+
continue
|
|
315
|
+
|
|
316
|
+
# 3. meta.json отсутствует или битый
|
|
317
|
+
meta_path = account_dir / "meta.json"
|
|
318
|
+
if not meta_path.exists():
|
|
319
|
+
meta = {"email": email, "password": ""}
|
|
320
|
+
if role == "worker":
|
|
321
|
+
meta["status"] = info.get("status", "created")
|
|
322
|
+
self._write_meta(account_dir, meta)
|
|
323
|
+
fixes.append(f"{role}: {email} — meta.json отсутствовал, создан заново")
|
|
324
|
+
else:
|
|
325
|
+
try:
|
|
326
|
+
json.loads(meta_path.read_text())
|
|
327
|
+
except (json.JSONDecodeError, OSError):
|
|
328
|
+
meta = {"email": email, "password": ""}
|
|
329
|
+
if role == "worker":
|
|
330
|
+
meta["status"] = info.get("status", "created")
|
|
331
|
+
self._write_meta(account_dir, meta)
|
|
332
|
+
fixes.append(f"{role}: {email} — meta.json был повреждён, пересоздан")
|
|
333
|
+
|
|
334
|
+
# 4. browser_profile отсутствует (данные есть, но куки нет)
|
|
335
|
+
for email, info in index.items():
|
|
336
|
+
account_id = info.get("id")
|
|
337
|
+
if not account_id:
|
|
338
|
+
continue
|
|
339
|
+
account_dir = role_dir / account_id
|
|
340
|
+
profile_dir = account_dir / "browser_profile"
|
|
341
|
+
if account_dir.exists() and not profile_dir.exists():
|
|
342
|
+
meta = self._read_meta(account_dir)
|
|
343
|
+
if meta.get("access_token") or meta.get("password"):
|
|
344
|
+
fixes.append(f"{role}: {email} — browser_profile отсутствует, нужен перелогин")
|
|
345
|
+
|
|
346
|
+
for email in to_remove:
|
|
347
|
+
index.pop(email, None)
|
|
348
|
+
changed = True
|
|
349
|
+
|
|
350
|
+
# 5. Сироты — папки есть, но в index их нет
|
|
351
|
+
if role_dir.exists():
|
|
352
|
+
indexed_ids = {info["id"] for info in index.values() if "id" in info}
|
|
353
|
+
for child in role_dir.iterdir():
|
|
354
|
+
if not child.is_dir() or child.name == "__pycache__":
|
|
355
|
+
continue
|
|
356
|
+
if child.name not in indexed_ids:
|
|
357
|
+
# Пробуем восстановить из meta.json
|
|
358
|
+
meta = self._read_meta(child)
|
|
359
|
+
email = meta.get("email")
|
|
360
|
+
if email and email not in index:
|
|
361
|
+
entry: dict[str, Any] = {"id": child.name}
|
|
362
|
+
if role == "worker":
|
|
363
|
+
entry["status"] = meta.get("status", "created")
|
|
364
|
+
entry["admin_email"] = meta.get("admin_email", "")
|
|
365
|
+
index[email] = entry
|
|
366
|
+
changed = True
|
|
367
|
+
fixes.append(f"{role}: папка {child.name} ({email}) — добавлена в index")
|
|
368
|
+
elif not email:
|
|
369
|
+
# Нет meta или нет email — удаляем сироту
|
|
370
|
+
shutil.rmtree(child)
|
|
371
|
+
fixes.append(f"{role}: папка-сирота {child.name} без meta — удалена")
|
|
372
|
+
|
|
373
|
+
if changed or to_remove:
|
|
374
|
+
self._write_index(role_dir, index)
|
|
375
|
+
|
|
376
|
+
return fixes
|
|
377
|
+
|
|
378
|
+
def _rebuild_index(self, role_dir: Path, role: str) -> dict:
|
|
379
|
+
"""Пересобрать index.json из существующих папок с meta.json."""
|
|
380
|
+
index: dict[str, Any] = {}
|
|
381
|
+
for child in role_dir.iterdir():
|
|
382
|
+
if not child.is_dir():
|
|
383
|
+
continue
|
|
384
|
+
meta = self._read_meta(child)
|
|
385
|
+
email = meta.get("email")
|
|
386
|
+
if email:
|
|
387
|
+
entry: dict[str, Any] = {"id": child.name}
|
|
388
|
+
if role == "worker":
|
|
389
|
+
entry["status"] = meta.get("status", "created")
|
|
390
|
+
entry["admin_email"] = meta.get("admin_email", "")
|
|
391
|
+
index[email] = entry
|
|
392
|
+
self._write_index(role_dir, index)
|
|
393
|
+
return index
|
|
394
|
+
|
|
395
|
+
# --- Миграция ---
|
|
396
|
+
|
|
397
|
+
def migrate_from_profiles(self) -> int:
|
|
398
|
+
"""Импорт из старого profiles.json в новую структуру. Возвращает кол-во мигрированных."""
|
|
399
|
+
profiles_json = PROJECT_ROOT / "profiles.json"
|
|
400
|
+
if not profiles_json.exists():
|
|
401
|
+
return 0
|
|
402
|
+
|
|
403
|
+
data = json.loads(profiles_json.read_text())
|
|
404
|
+
count = 0
|
|
405
|
+
|
|
406
|
+
# Админы — ключи верхнего уровня кроме _slots
|
|
407
|
+
for email, info in data.items():
|
|
408
|
+
if email.startswith("_"):
|
|
409
|
+
continue
|
|
410
|
+
if self.get_admin(email):
|
|
411
|
+
continue
|
|
412
|
+
admin = self.add_admin(email, info.get("password", ""))
|
|
413
|
+
admin.access_token = info.get("access_token")
|
|
414
|
+
admin.workspace_id = info.get("workspace_id")
|
|
415
|
+
admin.account_id = info.get("account_id")
|
|
416
|
+
admin.workspaces = info.get("workspaces")
|
|
417
|
+
self.update_admin(admin)
|
|
418
|
+
|
|
419
|
+
# Копируем browser_profile
|
|
420
|
+
old_profile = info.get("profile_dir")
|
|
421
|
+
if old_profile:
|
|
422
|
+
old_path = Path(old_profile)
|
|
423
|
+
if old_path.exists():
|
|
424
|
+
new_profile = self.admin_dir / admin.id / "browser_profile"
|
|
425
|
+
if not new_profile.exists():
|
|
426
|
+
shutil.copytree(old_path, new_profile)
|
|
427
|
+
count += 1
|
|
428
|
+
|
|
429
|
+
# Workers — из _slots
|
|
430
|
+
slots_data = data.get("_slots", {})
|
|
431
|
+
# Определяем admin_email (первый найденный админ)
|
|
432
|
+
admin_email = ""
|
|
433
|
+
for email in data:
|
|
434
|
+
if not email.startswith("_"):
|
|
435
|
+
admin_email = email
|
|
436
|
+
break
|
|
437
|
+
|
|
438
|
+
for email, info in slots_data.items():
|
|
439
|
+
if self.get_worker(email):
|
|
440
|
+
continue
|
|
441
|
+
worker = self.add_worker(email, info.get("password", ""), admin_email)
|
|
442
|
+
worker.status = info.get("status", "created")
|
|
443
|
+
worker.access_token = info.get("access_token")
|
|
444
|
+
worker.workspace_id = info.get("workspace_id")
|
|
445
|
+
self.update_worker(worker)
|
|
446
|
+
count += 1
|
|
447
|
+
|
|
448
|
+
return count
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .openai_web_auth import Page
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ChatGPTAPIError(Exception):
|
|
10
|
+
def __init__(self, status: int, message: str) -> None:
|
|
11
|
+
self.status = status
|
|
12
|
+
self.message = message
|
|
13
|
+
super().__init__(f"[{status}] {message}")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ChatGPTWorkspaceAPI:
|
|
17
|
+
"""Обёртка над ChatGPT backend-api — запросы выполняются через браузер (page.evaluate)."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, page: Page, account_id: str, access_token: str) -> None:
|
|
20
|
+
self.page = page
|
|
21
|
+
self.account_id = account_id
|
|
22
|
+
self.access_token = access_token
|
|
23
|
+
|
|
24
|
+
def _request(
|
|
25
|
+
self,
|
|
26
|
+
method: str,
|
|
27
|
+
path: str,
|
|
28
|
+
body: dict | None = None,
|
|
29
|
+
) -> Any:
|
|
30
|
+
"""Выполнить fetch() внутри браузера — куки и Cloudflare токены подхватываются автоматически."""
|
|
31
|
+
url = f"https://chatgpt.com{path}"
|
|
32
|
+
js_body = json.dumps(body) if body else "null"
|
|
33
|
+
|
|
34
|
+
result = self.page.evaluate(
|
|
35
|
+
"""async ([url, method, body, token, accountId]) => {
|
|
36
|
+
const opts = {
|
|
37
|
+
method: method,
|
|
38
|
+
headers: {
|
|
39
|
+
"Content-Type": "application/json",
|
|
40
|
+
"Authorization": "Bearer " + token,
|
|
41
|
+
"chatgpt-account-id": accountId,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
if (body && method !== 'GET' && method !== 'HEAD') opts.body = body;
|
|
45
|
+
const resp = await fetch(url, opts);
|
|
46
|
+
const text = await resp.text();
|
|
47
|
+
return {status: resp.status, body: text};
|
|
48
|
+
}""",
|
|
49
|
+
[url, method, js_body, self.access_token, self.account_id],
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
status = result["status"]
|
|
53
|
+
raw_body = result["body"]
|
|
54
|
+
|
|
55
|
+
if status >= 400:
|
|
56
|
+
short = raw_body[:200] if len(raw_body) > 200 else raw_body
|
|
57
|
+
if status == 403:
|
|
58
|
+
short = "Cloudflare/доступ запрещён — токен протух, перелогинитесь"
|
|
59
|
+
raise ChatGPTAPIError(status, short)
|
|
60
|
+
|
|
61
|
+
return json.loads(raw_body) if raw_body else {}
|
|
62
|
+
|
|
63
|
+
def send_invites(self, emails: list[str]) -> dict:
|
|
64
|
+
"""Отправить инвайты в workspace."""
|
|
65
|
+
return self._request(
|
|
66
|
+
"POST",
|
|
67
|
+
f"/backend-api/accounts/{self.account_id}/invites",
|
|
68
|
+
body={
|
|
69
|
+
"email_addresses": emails,
|
|
70
|
+
"role": "standard-user",
|
|
71
|
+
"resend_emails": True,
|
|
72
|
+
},
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def get_pending_invites(self) -> list[dict]:
|
|
76
|
+
"""Получить список ожидающих инвайтов."""
|
|
77
|
+
data = self._request(
|
|
78
|
+
"GET",
|
|
79
|
+
f"/backend-api/accounts/{self.account_id}/invites?offset=0&limit=100",
|
|
80
|
+
)
|
|
81
|
+
return data.get("invites", [])
|
|
82
|
+
|
|
83
|
+
def get_members(self) -> list[dict]:
|
|
84
|
+
"""Получить список участников workspace."""
|
|
85
|
+
data = self._request(
|
|
86
|
+
"GET",
|
|
87
|
+
f"/backend-api/accounts/{self.account_id}/users?offset=0&limit=100",
|
|
88
|
+
)
|
|
89
|
+
return data.get("items", data.get("users", []))
|
|
90
|
+
|
|
91
|
+
def delete_member(self, user_id: str) -> dict:
|
|
92
|
+
"""Удалить участника из workspace по user_id."""
|
|
93
|
+
return self._request(
|
|
94
|
+
"DELETE",
|
|
95
|
+
f"/backend-api/accounts/{self.account_id}/users/{user_id}",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def delete_invite(self, email: str) -> dict:
|
|
99
|
+
"""Удалить инвайт."""
|
|
100
|
+
return self._request(
|
|
101
|
+
"DELETE",
|
|
102
|
+
f"/backend-api/accounts/{self.account_id}/invites",
|
|
103
|
+
body={"email_address": email},
|
|
104
|
+
)
|
package/backend/dto.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict, dataclass
|
|
4
|
+
|
|
5
|
+
from .account_store import AdminAccount, WorkerAccount
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class AdminRow:
|
|
10
|
+
email: str
|
|
11
|
+
has_access_token: bool
|
|
12
|
+
has_browser_profile: bool
|
|
13
|
+
workspace_id: str | None
|
|
14
|
+
workspace_count: int
|
|
15
|
+
created_at: str | None
|
|
16
|
+
last_login: str | None
|
|
17
|
+
status_label: str
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def from_account(
|
|
21
|
+
cls,
|
|
22
|
+
account: AdminAccount,
|
|
23
|
+
*,
|
|
24
|
+
has_browser_profile: bool = False,
|
|
25
|
+
) -> "AdminRow":
|
|
26
|
+
has_access_token = bool(account.access_token)
|
|
27
|
+
workspace_count = len(account.workspaces or [])
|
|
28
|
+
if has_access_token and has_browser_profile:
|
|
29
|
+
status_label = "Готов"
|
|
30
|
+
elif has_access_token:
|
|
31
|
+
status_label = "Есть токен"
|
|
32
|
+
elif has_browser_profile:
|
|
33
|
+
status_label = "Нужен вход"
|
|
34
|
+
else:
|
|
35
|
+
status_label = "Не настроен"
|
|
36
|
+
|
|
37
|
+
return cls(
|
|
38
|
+
email=account.email,
|
|
39
|
+
has_access_token=has_access_token,
|
|
40
|
+
has_browser_profile=has_browser_profile,
|
|
41
|
+
workspace_id=account.workspace_id,
|
|
42
|
+
workspace_count=workspace_count,
|
|
43
|
+
created_at=account.created_at,
|
|
44
|
+
last_login=account.last_login,
|
|
45
|
+
status_label=status_label,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class WorkerRow:
|
|
51
|
+
email: str
|
|
52
|
+
status: str
|
|
53
|
+
has_access_token: bool
|
|
54
|
+
has_browser_profile: bool
|
|
55
|
+
workspace_id: str | None
|
|
56
|
+
admin_email: str | None
|
|
57
|
+
has_openai_password: bool
|
|
58
|
+
created_at: str | None
|
|
59
|
+
status_label: str
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def from_account(
|
|
63
|
+
cls,
|
|
64
|
+
account: WorkerAccount,
|
|
65
|
+
*,
|
|
66
|
+
has_browser_profile: bool = False,
|
|
67
|
+
) -> "WorkerRow":
|
|
68
|
+
has_access_token = bool(account.access_token)
|
|
69
|
+
if has_access_token and has_browser_profile:
|
|
70
|
+
status_label = "Готов"
|
|
71
|
+
elif account.status == "registered":
|
|
72
|
+
status_label = "Зарегистрирован"
|
|
73
|
+
elif account.status == "invited":
|
|
74
|
+
status_label = "Инвайт отправлен"
|
|
75
|
+
elif account.status == "created":
|
|
76
|
+
status_label = "Создан"
|
|
77
|
+
else:
|
|
78
|
+
status_label = account.status
|
|
79
|
+
|
|
80
|
+
return cls(
|
|
81
|
+
email=account.email,
|
|
82
|
+
status=account.status,
|
|
83
|
+
has_access_token=has_access_token,
|
|
84
|
+
has_browser_profile=has_browser_profile,
|
|
85
|
+
workspace_id=account.workspace_id,
|
|
86
|
+
admin_email=account.admin_email,
|
|
87
|
+
has_openai_password=bool(account.openai_password),
|
|
88
|
+
created_at=account.created_at,
|
|
89
|
+
status_label=status_label,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class MailAccountRow:
|
|
95
|
+
kind: str
|
|
96
|
+
email: str
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass
|
|
100
|
+
class AppStateDTO:
|
|
101
|
+
admins: list[AdminRow]
|
|
102
|
+
workers: list[WorkerRow]
|
|
103
|
+
accounts: list[MailAccountRow]
|
|
104
|
+
|
|
105
|
+
def to_dict(self) -> dict:
|
|
106
|
+
return asdict(self)
|