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,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class RPCRequest:
|
|
10
|
+
request_id: str
|
|
11
|
+
method: str
|
|
12
|
+
params: dict[str, Any]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RPCError(Exception):
|
|
16
|
+
def __init__(self, code: int, message: str, data: dict[str, Any] | None = None) -> None:
|
|
17
|
+
self.code = code
|
|
18
|
+
self.message = message
|
|
19
|
+
self.data = data or {}
|
|
20
|
+
super().__init__(message)
|
|
21
|
+
|
|
22
|
+
def to_dict(self) -> dict[str, Any]:
|
|
23
|
+
out = {
|
|
24
|
+
"code": self.code,
|
|
25
|
+
"message": self.message,
|
|
26
|
+
}
|
|
27
|
+
if self.data:
|
|
28
|
+
out["data"] = self.data
|
|
29
|
+
return out
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def parse_request(line: str) -> RPCRequest:
|
|
33
|
+
try:
|
|
34
|
+
payload = json.loads(line)
|
|
35
|
+
except json.JSONDecodeError as e:
|
|
36
|
+
raise RPCError(-32700, "Parse error", {"details": str(e)}) from e
|
|
37
|
+
|
|
38
|
+
if not isinstance(payload, dict):
|
|
39
|
+
raise RPCError(-32600, "Invalid request", {"details": "payload must be object"})
|
|
40
|
+
|
|
41
|
+
request_id = payload.get("id")
|
|
42
|
+
method = payload.get("method")
|
|
43
|
+
params = payload.get("params", {})
|
|
44
|
+
|
|
45
|
+
if not isinstance(request_id, str) or not request_id:
|
|
46
|
+
raise RPCError(-32600, "Invalid request", {"details": "id must be non-empty string"})
|
|
47
|
+
if not isinstance(method, str) or not method:
|
|
48
|
+
raise RPCError(-32600, "Invalid request", {"details": "method must be non-empty string"})
|
|
49
|
+
if not isinstance(params, dict):
|
|
50
|
+
raise RPCError(-32602, "Invalid params", {"details": "params must be object"})
|
|
51
|
+
|
|
52
|
+
return RPCRequest(request_id=request_id, method=method, params=params)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def make_success_response(request_id: str, result: Any) -> dict[str, Any]:
|
|
56
|
+
return {
|
|
57
|
+
"type": "response",
|
|
58
|
+
"id": request_id,
|
|
59
|
+
"ok": True,
|
|
60
|
+
"result": result,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def make_error_response(request_id: str, error: RPCError) -> dict[str, Any]:
|
|
65
|
+
return {
|
|
66
|
+
"type": "response",
|
|
67
|
+
"id": request_id,
|
|
68
|
+
"ok": False,
|
|
69
|
+
"error": error.to_dict(),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def make_event(event: str, data: dict[str, Any]) -> dict[str, Any]:
|
|
74
|
+
return {
|
|
75
|
+
"type": "event",
|
|
76
|
+
"event": event,
|
|
77
|
+
"data": data,
|
|
78
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
import threading
|
|
6
|
+
import traceback
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .file_logger import FileLogger
|
|
10
|
+
from .jobs import JobManager
|
|
11
|
+
from .rpc_protocol import (
|
|
12
|
+
RPCError,
|
|
13
|
+
RPCRequest,
|
|
14
|
+
make_error_response,
|
|
15
|
+
make_event,
|
|
16
|
+
make_success_response,
|
|
17
|
+
parse_request,
|
|
18
|
+
)
|
|
19
|
+
from .ui_facade import UIFacade
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RPCServer:
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
self.logger = FileLogger()
|
|
25
|
+
self.facade = UIFacade()
|
|
26
|
+
self._out_lock = threading.Lock()
|
|
27
|
+
self.jobs = JobManager(self.emit_event, file_logger=self.logger)
|
|
28
|
+
self.logger.info("RPC server initialized")
|
|
29
|
+
|
|
30
|
+
def emit_event(self, event: str, data: dict[str, Any]) -> None:
|
|
31
|
+
self._write_json(make_event(event, data))
|
|
32
|
+
|
|
33
|
+
def _write_json(self, payload: dict[str, Any]) -> None:
|
|
34
|
+
line = json.dumps(payload, ensure_ascii=False)
|
|
35
|
+
with self._out_lock:
|
|
36
|
+
sys.stdout.write(line + "\n")
|
|
37
|
+
sys.stdout.flush()
|
|
38
|
+
|
|
39
|
+
def _as_str_param(self, params: dict[str, Any], key: str) -> str:
|
|
40
|
+
value = params.get(key)
|
|
41
|
+
if not isinstance(value, str) or not value:
|
|
42
|
+
raise RPCError(-32602, "Invalid params", {"details": f"'{key}' must be non-empty string"})
|
|
43
|
+
return value
|
|
44
|
+
|
|
45
|
+
def _as_int_param(self, params: dict[str, Any], key: str) -> int:
|
|
46
|
+
value = params.get(key)
|
|
47
|
+
if not isinstance(value, int):
|
|
48
|
+
raise RPCError(-32602, "Invalid params", {"details": f"'{key}' must be integer"})
|
|
49
|
+
return value
|
|
50
|
+
|
|
51
|
+
def _run_job(self, title: str, fn):
|
|
52
|
+
return self.jobs.start(title, fn)
|
|
53
|
+
|
|
54
|
+
def _handle_request(self, req: RPCRequest) -> dict[str, Any]:
|
|
55
|
+
m = req.method
|
|
56
|
+
p = req.params
|
|
57
|
+
|
|
58
|
+
if m == "ping":
|
|
59
|
+
return make_success_response(req.request_id, {"pong": True})
|
|
60
|
+
|
|
61
|
+
if m == "state.get":
|
|
62
|
+
return make_success_response(req.request_id, self.facade.get_state())
|
|
63
|
+
|
|
64
|
+
if m == "admins.list":
|
|
65
|
+
return make_success_response(req.request_id, {"items": self.facade.list_admins()})
|
|
66
|
+
|
|
67
|
+
if m == "workers.list":
|
|
68
|
+
return make_success_response(req.request_id, {"items": self.facade.list_workers()})
|
|
69
|
+
|
|
70
|
+
if m == "workers.by_admin":
|
|
71
|
+
email = self._as_str_param(p, "admin_email")
|
|
72
|
+
return make_success_response(req.request_id, {"items": self.facade.list_workers_by_admin(email)})
|
|
73
|
+
|
|
74
|
+
if m == "accounts.mail.list":
|
|
75
|
+
return make_success_response(req.request_id, {"items": self.facade.list_mail_accounts()})
|
|
76
|
+
|
|
77
|
+
if m == "admin.add":
|
|
78
|
+
email = self._as_str_param(p, "email")
|
|
79
|
+
password = self._as_str_param(p, "password")
|
|
80
|
+
item = self.facade.add_admin(email, password)
|
|
81
|
+
return make_success_response(req.request_id, {"item": item})
|
|
82
|
+
|
|
83
|
+
if m == "job.add_admin_manual":
|
|
84
|
+
job_id = self._run_job(
|
|
85
|
+
"Ручное добавление админа",
|
|
86
|
+
lambda ctx: self.facade.add_admin_manual(ctx.log),
|
|
87
|
+
)
|
|
88
|
+
return make_success_response(req.request_id, {"job_id": job_id})
|
|
89
|
+
|
|
90
|
+
if m == "admin.delete":
|
|
91
|
+
email = self._as_str_param(p, "email")
|
|
92
|
+
self.facade.delete_admin(email)
|
|
93
|
+
return make_success_response(req.request_id, {"deleted": True})
|
|
94
|
+
|
|
95
|
+
if m == "worker.delete":
|
|
96
|
+
email = self._as_str_param(p, "email")
|
|
97
|
+
self.facade.delete_worker(email)
|
|
98
|
+
return make_success_response(req.request_id, {"deleted": True})
|
|
99
|
+
|
|
100
|
+
if m == "job.login_admin":
|
|
101
|
+
email = self._as_str_param(p, "email")
|
|
102
|
+
job_id = self._run_job(
|
|
103
|
+
f"Логин админа: {email}",
|
|
104
|
+
lambda ctx: self.facade.login_admin(email, ctx.log),
|
|
105
|
+
)
|
|
106
|
+
return make_success_response(req.request_id, {"job_id": job_id})
|
|
107
|
+
|
|
108
|
+
if m == "job.login_admin_manual":
|
|
109
|
+
email = self._as_str_param(p, "email")
|
|
110
|
+
job_id = self._run_job(
|
|
111
|
+
f"Ручной логин админа: {email}",
|
|
112
|
+
lambda ctx: self.facade.login_admin_manual(email, ctx.log),
|
|
113
|
+
)
|
|
114
|
+
return make_success_response(req.request_id, {"job_id": job_id})
|
|
115
|
+
|
|
116
|
+
if m == "job.open_admin_browser":
|
|
117
|
+
email = self._as_str_param(p, "email")
|
|
118
|
+
job_id = self._run_job(
|
|
119
|
+
f"Браузер админа: {email}",
|
|
120
|
+
lambda ctx: self.facade.open_admin_browser(email, ctx.log),
|
|
121
|
+
)
|
|
122
|
+
return make_success_response(req.request_id, {"job_id": job_id})
|
|
123
|
+
|
|
124
|
+
if m == "job.run_slots":
|
|
125
|
+
admin_email = self._as_str_param(p, "admin_email")
|
|
126
|
+
count = self._as_int_param(p, "count")
|
|
127
|
+
if count <= 0:
|
|
128
|
+
raise RPCError(-32602, "Invalid params", {"details": "'count' must be > 0"})
|
|
129
|
+
job_id = self._run_job(
|
|
130
|
+
f"Пайплайн слотов ({admin_email})",
|
|
131
|
+
lambda ctx: self.facade.run_slots_pipeline(
|
|
132
|
+
admin_email,
|
|
133
|
+
count,
|
|
134
|
+
ctx.log,
|
|
135
|
+
progress=ctx.progress,
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
return make_success_response(req.request_id, {"job_id": job_id})
|
|
139
|
+
|
|
140
|
+
if m == "job.relogin_worker":
|
|
141
|
+
email = self._as_str_param(p, "email")
|
|
142
|
+
job_id = self._run_job(
|
|
143
|
+
f"Перелогин: {email}",
|
|
144
|
+
lambda ctx: self.facade.relogin_worker_email(email, ctx.log),
|
|
145
|
+
)
|
|
146
|
+
return make_success_response(req.request_id, {"job_id": job_id})
|
|
147
|
+
|
|
148
|
+
if m == "job.relogin_all_workers":
|
|
149
|
+
job_id = self._run_job(
|
|
150
|
+
"Перелогин всех слотов",
|
|
151
|
+
lambda ctx: self.facade.relogin_all_workers(ctx.log, progress=ctx.progress),
|
|
152
|
+
)
|
|
153
|
+
return make_success_response(req.request_id, {"job_id": job_id})
|
|
154
|
+
|
|
155
|
+
if m == "job.open_worker_browser":
|
|
156
|
+
email = self._as_str_param(p, "email")
|
|
157
|
+
job_id = self._run_job(
|
|
158
|
+
f"Браузер слота: {email}",
|
|
159
|
+
lambda ctx: self.facade.open_worker_browser(email, ctx.log),
|
|
160
|
+
)
|
|
161
|
+
return make_success_response(req.request_id, {"job_id": job_id})
|
|
162
|
+
|
|
163
|
+
if m == "job.fetch_mail":
|
|
164
|
+
email = self._as_str_param(p, "email")
|
|
165
|
+
job_id = self._run_job(
|
|
166
|
+
f"Почта: {email}",
|
|
167
|
+
lambda ctx: self.facade.fetch_mail(email, ctx.log),
|
|
168
|
+
)
|
|
169
|
+
return make_success_response(req.request_id, {"job_id": job_id})
|
|
170
|
+
|
|
171
|
+
if m == "shutdown":
|
|
172
|
+
self.facade.shutdown()
|
|
173
|
+
return make_success_response(req.request_id, {"ok": True})
|
|
174
|
+
|
|
175
|
+
raise RPCError(-32601, "Method not found", {"method": m})
|
|
176
|
+
|
|
177
|
+
def serve(self) -> None:
|
|
178
|
+
for raw in sys.stdin:
|
|
179
|
+
line = raw.strip()
|
|
180
|
+
if not line:
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
req: RPCRequest | None = None
|
|
184
|
+
try:
|
|
185
|
+
req = parse_request(line)
|
|
186
|
+
response = self._handle_request(req)
|
|
187
|
+
except RPCError as e:
|
|
188
|
+
self.logger.error(f"RPC error for request {req.request_id if req else 'unknown'}: {e}")
|
|
189
|
+
request_id = req.request_id if req else "unknown"
|
|
190
|
+
if request_id == "unknown":
|
|
191
|
+
try:
|
|
192
|
+
maybe = json.loads(line)
|
|
193
|
+
if isinstance(maybe, dict) and isinstance(maybe.get("id"), str):
|
|
194
|
+
request_id = maybe["id"]
|
|
195
|
+
except Exception:
|
|
196
|
+
pass
|
|
197
|
+
response = make_error_response(request_id, e)
|
|
198
|
+
except Exception as e:
|
|
199
|
+
tb = traceback.format_exc()
|
|
200
|
+
self.logger.error(f"Unhandled server error: {e}", traceback_text=tb)
|
|
201
|
+
request_id = req.request_id if req else "unknown"
|
|
202
|
+
response = make_error_response(
|
|
203
|
+
request_id,
|
|
204
|
+
RPCError(
|
|
205
|
+
-32000,
|
|
206
|
+
"Server error",
|
|
207
|
+
{
|
|
208
|
+
"details": str(e),
|
|
209
|
+
"traceback": tb,
|
|
210
|
+
},
|
|
211
|
+
),
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
self._write_json(response)
|
|
215
|
+
|
|
216
|
+
self.facade.shutdown()
|
|
217
|
+
self.logger.info("RPC server stopped")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def main() -> int:
|
|
221
|
+
from pathlib import Path
|
|
222
|
+
|
|
223
|
+
from dotenv import load_dotenv
|
|
224
|
+
|
|
225
|
+
load_dotenv(Path(__file__).resolve().parent.parent / ".env")
|
|
226
|
+
|
|
227
|
+
server = RPCServer()
|
|
228
|
+
server.serve()
|
|
229
|
+
return 0
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
if __name__ == "__main__":
|
|
233
|
+
raise SystemExit(main())
|