izteamslots 1.1.6 → 1.3.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 -25
- package/backend/rpc_server.py +73 -1
- package/package.json +1 -1
- package/ui/src/menus/mainMenus.ts +16 -1
- package/ui/src/menus/types.ts +9 -0
- package/ui/src/screens/MainScreen.ts +32 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [1.3.0](https://github.com/izzzzzi/izTeamSlots/compare/v1.2.0...v1.3.0) (2026-03-06)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* add Settings menu for API keys and mail providers ([e026db7](https://github.com/izzzzzi/izTeamSlots/commit/e026db7dd01db0544ad7993dfb6ab020490f2ae8))
|
|
7
|
+
|
|
8
|
+
# [1.2.0](https://github.com/izzzzzi/izTeamSlots/compare/v1.1.6...v1.2.0) (2026-03-06)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* load config from ~/.izteamslots/.env, update README install docs ([99ca787](https://github.com/izzzzzi/izTeamSlots/commit/99ca787207d65cfde29a9b0bd173dc870ca59ee6))
|
|
14
|
+
|
|
1
15
|
## [1.1.6](https://github.com/izzzzzi/izTeamSlots/compare/v1.1.5...v1.1.6) (2026-03-06)
|
|
2
16
|
|
|
3
17
|
|
package/README.md
CHANGED
|
@@ -83,14 +83,12 @@ izTeamSlots/
|
|
|
83
83
|
### npm (рекомендуется)
|
|
84
84
|
|
|
85
85
|
```bash
|
|
86
|
-
npm install -g izteamslots
|
|
86
|
+
npm install -g izteamslots@latest
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
Установщик автоматически поставит Python-зависимости (через [uv](https://docs.astral.sh/uv/)), [Bun](https://bun.sh) и всё остальное.
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
npx izteamslots
|
|
93
|
-
```
|
|
91
|
+
> Chrome и chromedriver скачиваются автоматически при первом запуске через SeleniumBase.
|
|
94
92
|
|
|
95
93
|
### Из исходников
|
|
96
94
|
|
|
@@ -100,32 +98,27 @@ cd izTeamSlots
|
|
|
100
98
|
npm install
|
|
101
99
|
```
|
|
102
100
|
|
|
103
|
-
|
|
104
|
-
- Проверит Python 3.11+
|
|
105
|
-
- Установит [uv](https://docs.astral.sh/uv/) если его нет (быстрый Python package manager)
|
|
106
|
-
- Установит Python-зависимости через uv (`seleniumbase`, `requests`)
|
|
107
|
-
- Установит [Bun](https://bun.sh) если его нет + UI-зависимости
|
|
108
|
-
- Создаст `.env` из `.env.example` если его нет
|
|
101
|
+
### Настройка
|
|
109
102
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
Если нужно перезапустить setup вручную:
|
|
103
|
+
Создайте файл конфигурации `~/.izteamslots/.env`:
|
|
113
104
|
|
|
114
105
|
```bash
|
|
115
|
-
|
|
116
|
-
|
|
106
|
+
# Linux / macOS
|
|
107
|
+
mkdir -p ~/.izteamslots
|
|
108
|
+
echo "BOOMLIFY_API_KEY=your_api_key" > ~/.izteamslots/.env
|
|
117
109
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
# Temp mail API (обязательно для создания слотов)
|
|
122
|
-
BOOMLIFY_API_KEY=your_api_key
|
|
110
|
+
# Windows (PowerShell)
|
|
111
|
+
mkdir "$env:USERPROFILE\.izteamslots" -Force
|
|
112
|
+
echo "BOOMLIFY_API_KEY=your_api_key" > "$env:USERPROFILE\.izteamslots\.env"
|
|
123
113
|
```
|
|
124
114
|
|
|
115
|
+
Конфиг загружается в порядке приоритета: `~/.izteamslots/.env` > `./.env` > встроенный.
|
|
116
|
+
|
|
125
117
|
Опциональные переменные:
|
|
126
118
|
|
|
127
119
|
| Переменная | По умолчанию | Описание |
|
|
128
120
|
|-----------|:------------:|----------|
|
|
121
|
+
| `BOOMLIFY_API_KEY` | — | API-ключ Boomlify (обязательно для слотов) |
|
|
129
122
|
| `BOOMLIFY_DOMAIN` | авто | Домен для временных почт |
|
|
130
123
|
| `BOOMLIFY_TIME` | `permanent` | Время жизни ящика |
|
|
131
124
|
| `SLOT_MAIL_PROVIDER` | `boomlify` | Провайдер почты для слотов |
|
|
@@ -134,13 +127,11 @@ BOOMLIFY_API_KEY=your_api_key
|
|
|
134
127
|
## Запуск
|
|
135
128
|
|
|
136
129
|
```bash
|
|
137
|
-
# Глобальная установка
|
|
138
130
|
izteamslots
|
|
139
|
-
|
|
140
|
-
# Из исходников
|
|
141
|
-
npm start
|
|
142
131
|
```
|
|
143
132
|
|
|
133
|
+
Из исходников: `npm start`
|
|
134
|
+
|
|
144
135
|
Это запустит OpenTUI frontend через **Bun**, который поднимет Python RPC backend (`python -m backend`) по `stdio`.
|
|
145
136
|
|
|
146
137
|
---
|
package/backend/rpc_server.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import os
|
|
4
5
|
import sys
|
|
5
6
|
import threading
|
|
6
7
|
import traceback
|
|
8
|
+
from pathlib import Path
|
|
7
9
|
from typing import Any
|
|
8
10
|
|
|
9
11
|
from .file_logger import FileLogger
|
|
@@ -51,6 +53,59 @@ class RPCServer:
|
|
|
51
53
|
def _run_job(self, title: str, fn):
|
|
52
54
|
return self.jobs.start(title, fn)
|
|
53
55
|
|
|
56
|
+
_SETTINGS_KEYS = [
|
|
57
|
+
("BOOMLIFY_API_KEY", "Boomlify API ключ"),
|
|
58
|
+
("BOOMLIFY_DOMAIN", "Домен временной почты"),
|
|
59
|
+
("BOOMLIFY_TIME", "Время жизни ящика"),
|
|
60
|
+
("SLOT_MAIL_PROVIDER", "Провайдер почты для слотов"),
|
|
61
|
+
("MAIL_PROVIDER", "Провайдер почты для админов"),
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def _settings_path() -> Path:
|
|
66
|
+
return Path.home() / ".izteamslots" / ".env"
|
|
67
|
+
|
|
68
|
+
def _get_settings(self) -> dict[str, Any]:
|
|
69
|
+
items = []
|
|
70
|
+
for key, label in self._SETTINGS_KEYS:
|
|
71
|
+
value = os.environ.get(key, "")
|
|
72
|
+
masked = ""
|
|
73
|
+
if value:
|
|
74
|
+
masked = value[:4] + "***" + value[-4:] if len(value) > 12 else "***"
|
|
75
|
+
items.append({"key": key, "label": label, "value": value, "masked": masked})
|
|
76
|
+
return {"items": items, "path": str(self._settings_path())}
|
|
77
|
+
|
|
78
|
+
def _set_setting(self, key: str, value: str) -> None:
|
|
79
|
+
allowed = {k for k, _ in self._SETTINGS_KEYS}
|
|
80
|
+
if key not in allowed:
|
|
81
|
+
raise RPCError(-32602, "Unknown setting", {"key": key})
|
|
82
|
+
|
|
83
|
+
env_path = self._settings_path()
|
|
84
|
+
env_path.parent.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
|
|
86
|
+
# Read existing lines
|
|
87
|
+
lines: list[str] = []
|
|
88
|
+
if env_path.is_file():
|
|
89
|
+
lines = env_path.read_text(encoding="utf-8").splitlines()
|
|
90
|
+
|
|
91
|
+
# Update or append
|
|
92
|
+
found = False
|
|
93
|
+
for i, line in enumerate(lines):
|
|
94
|
+
if line.startswith(f"{key}=") or line.startswith(f"{key} ="):
|
|
95
|
+
lines[i] = f"{key}={value}"
|
|
96
|
+
found = True
|
|
97
|
+
break
|
|
98
|
+
if not found:
|
|
99
|
+
lines.append(f"{key}={value}")
|
|
100
|
+
|
|
101
|
+
env_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
102
|
+
|
|
103
|
+
# Update runtime env
|
|
104
|
+
if value:
|
|
105
|
+
os.environ[key] = value
|
|
106
|
+
elif key in os.environ:
|
|
107
|
+
del os.environ[key]
|
|
108
|
+
|
|
54
109
|
def _handle_request(self, req: RPCRequest) -> dict[str, Any]:
|
|
55
110
|
m = req.method
|
|
56
111
|
p = req.params
|
|
@@ -168,6 +223,15 @@ class RPCServer:
|
|
|
168
223
|
)
|
|
169
224
|
return make_success_response(req.request_id, {"job_id": job_id})
|
|
170
225
|
|
|
226
|
+
if m == "settings.get":
|
|
227
|
+
return make_success_response(req.request_id, self._get_settings())
|
|
228
|
+
|
|
229
|
+
if m == "settings.set":
|
|
230
|
+
key = self._as_str_param(p, "key")
|
|
231
|
+
value = self._as_str_param(p, "value")
|
|
232
|
+
self._set_setting(key, value)
|
|
233
|
+
return make_success_response(req.request_id, {"ok": True})
|
|
234
|
+
|
|
171
235
|
if m == "shutdown":
|
|
172
236
|
self.facade.shutdown()
|
|
173
237
|
return make_success_response(req.request_id, {"ok": True})
|
|
@@ -222,7 +286,15 @@ def main() -> int:
|
|
|
222
286
|
|
|
223
287
|
from dotenv import load_dotenv
|
|
224
288
|
|
|
225
|
-
|
|
289
|
+
# Priority: ~/.izteamslots/.env > CWD/.env > package .env
|
|
290
|
+
home_env = Path.home() / ".izteamslots" / ".env"
|
|
291
|
+
cwd_env = Path.cwd() / ".env"
|
|
292
|
+
pkg_env = Path(__file__).resolve().parent.parent / ".env"
|
|
293
|
+
|
|
294
|
+
for env_path in (home_env, cwd_env, pkg_env):
|
|
295
|
+
if env_path.is_file():
|
|
296
|
+
load_dotenv(env_path)
|
|
297
|
+
break
|
|
226
298
|
|
|
227
299
|
server = RPCServer()
|
|
228
300
|
server.serve()
|
package/package.json
CHANGED
|
@@ -6,6 +6,7 @@ export function getMenuOptions(menuName: MenuName, state: AppState): MenuOption[
|
|
|
6
6
|
{ id: "menu_admins", label: "Админы", hint: "Доступы, токены, браузерные профили" },
|
|
7
7
|
{ id: "menu_slots", label: "Слоты", hint: "Создание, перелогин и обслуживание" },
|
|
8
8
|
{ id: "menu_mail", label: "Почта", hint: "Ящики и входящие письма" },
|
|
9
|
+
{ id: "menu_settings", label: "Настройки", hint: "API-ключи и провайдеры почты" },
|
|
9
10
|
{ id: "menu_exit", label: "Выход", hint: "Закрыть приложение" },
|
|
10
11
|
]
|
|
11
12
|
}
|
|
@@ -62,6 +63,16 @@ export function getMenuOptions(menuName: MenuName, state: AppState): MenuOption[
|
|
|
62
63
|
}))
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
if (menuName === "settings") {
|
|
67
|
+
return [
|
|
68
|
+
...(state.settings ?? []).map((s: { key: string; label: string; masked: string }) => ({
|
|
69
|
+
id: `setting:${s.key}`,
|
|
70
|
+
label: s.label,
|
|
71
|
+
hint: s.masked || "не задан",
|
|
72
|
+
})),
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
|
|
65
76
|
if (menuName === "confirm") {
|
|
66
77
|
return [
|
|
67
78
|
{ id: "confirm_yes", label: "Да, удалить", hint: "Подтвердить необратимое действие", destructive: true },
|
|
@@ -110,6 +121,7 @@ export function getScreenTitle(menuName: MenuName, ctx: MenuContext): string {
|
|
|
110
121
|
if (menuName === "admins") return "Администраторы"
|
|
111
122
|
if (menuName === "slots") return "Слоты"
|
|
112
123
|
if (menuName === "mail") return "Почтовые ящики"
|
|
124
|
+
if (menuName === "settings") return "Настройки"
|
|
113
125
|
if (menuName === "pick_admin") return ctx.title ?? "Выбор администратора"
|
|
114
126
|
if (menuName === "pick_worker") return ctx.title ?? "Выбор слота"
|
|
115
127
|
if (menuName === "pick_account") return ctx.title ?? "Выбор аккаунта"
|
|
@@ -140,6 +152,8 @@ export function getScreenDescription(menuName: MenuName, state: AppState, ctx: M
|
|
|
140
152
|
: "Выберите аккаунт слева, чтобы забрать входящие письма."
|
|
141
153
|
}
|
|
142
154
|
|
|
155
|
+
if (menuName === "settings") return "Настройте API-ключи и провайдеры почты. Enter для редактирования."
|
|
156
|
+
|
|
143
157
|
if (menuName === "pick_admin") return ctx.title ?? "Выберите администратора для следующего действия."
|
|
144
158
|
if (menuName === "pick_worker") return ctx.title ?? "Выберите слот для следующего действия."
|
|
145
159
|
if (menuName === "pick_account") return ctx.title ?? "Выберите аккаунт."
|
|
@@ -200,7 +214,7 @@ export function getTable(menuName: MenuName, state: AppState, ctx: MenuContext):
|
|
|
200
214
|
}
|
|
201
215
|
|
|
202
216
|
export function getHint(menuName: MenuName, _ctx: MenuContext): string {
|
|
203
|
-
if (menuName === "main") return "↑↓ или 1-
|
|
217
|
+
if (menuName === "main") return "↑↓ или 1-5: выбор Enter: открыть r: обновить q: выход"
|
|
204
218
|
if (menuName === "confirm") return "Enter: подтвердить Esc: отмена"
|
|
205
219
|
return "↑↓ или 1-9: выбор Enter: действие Esc: назад r: обновить y: копировать лог"
|
|
206
220
|
}
|
|
@@ -212,6 +226,7 @@ export function parentMenu(menuName: MenuName, ctx: MenuContext): MenuName {
|
|
|
212
226
|
admins: "main",
|
|
213
227
|
slots: "main",
|
|
214
228
|
mail: "main",
|
|
229
|
+
settings: "main",
|
|
215
230
|
pick_admin: "admins",
|
|
216
231
|
pick_worker: "slots",
|
|
217
232
|
pick_account: "mail",
|
package/ui/src/menus/types.ts
CHANGED
|
@@ -3,6 +3,7 @@ export type MenuName =
|
|
|
3
3
|
| "admins"
|
|
4
4
|
| "slots"
|
|
5
5
|
| "mail"
|
|
6
|
+
| "settings"
|
|
6
7
|
| "pick_admin"
|
|
7
8
|
| "pick_worker"
|
|
8
9
|
| "pick_account"
|
|
@@ -46,10 +47,18 @@ export interface MailAccountRow {
|
|
|
46
47
|
email: string
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
export interface SettingItem {
|
|
51
|
+
key: string
|
|
52
|
+
label: string
|
|
53
|
+
value: string
|
|
54
|
+
masked: string
|
|
55
|
+
}
|
|
56
|
+
|
|
49
57
|
export interface AppState {
|
|
50
58
|
admins: AdminRow[]
|
|
51
59
|
workers: WorkerRow[]
|
|
52
60
|
accounts: MailAccountRow[]
|
|
61
|
+
settings?: SettingItem[]
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
export interface MenuContext {
|
|
@@ -127,6 +127,7 @@ function getMenuOptionDescription(option: MenuOption | undefined): string {
|
|
|
127
127
|
if (optionId === "menu_admins") return "Добавление, логин и обслуживание администраторов."
|
|
128
128
|
if (optionId === "menu_slots") return "Создание слотов, перелогин и работа с профилями."
|
|
129
129
|
if (optionId === "menu_mail") return "Проверка входящих писем админов и слотов."
|
|
130
|
+
if (optionId === "menu_settings") return "API-ключи и настройки провайдеров почты."
|
|
130
131
|
if (optionId === "menu_exit") return "Аккуратно завершить интерфейс."
|
|
131
132
|
if (optionId === "adm_add") return "Создать нового админа: в авто-режиме ввод в TUI, в ручном режиме вход сразу в браузере."
|
|
132
133
|
if (optionId === "adm_relogin") return "Выбрать админа и режим перелогина: автоматический или ручной."
|
|
@@ -372,6 +373,17 @@ export class MainScreen {
|
|
|
372
373
|
this.render()
|
|
373
374
|
}
|
|
374
375
|
|
|
376
|
+
private async loadSettings() {
|
|
377
|
+
try {
|
|
378
|
+
const result = await this.rpc.request<{ items: Array<{ key: string; label: string; value: string; masked: string }> }>("settings.get")
|
|
379
|
+
this.state = { ...this.state, settings: result.items }
|
|
380
|
+
} catch (error) {
|
|
381
|
+
this.pushLog(`Ошибка загрузки настроек: ${String(error)}`)
|
|
382
|
+
}
|
|
383
|
+
this.rebuildMenuOptions()
|
|
384
|
+
this.render()
|
|
385
|
+
}
|
|
386
|
+
|
|
375
387
|
private rebuildMenuOptions() {
|
|
376
388
|
this.menuOptions = getMenuOptions(this.menuName, this.state)
|
|
377
389
|
if (this.selectedIndex >= this.menuOptions.length) {
|
|
@@ -833,6 +845,10 @@ export class MainScreen {
|
|
|
833
845
|
if (optionId === "menu_admins") this.menuName = "admins"
|
|
834
846
|
else if (optionId === "menu_slots") this.menuName = "slots"
|
|
835
847
|
else if (optionId === "menu_mail") this.menuName = "mail"
|
|
848
|
+
else if (optionId === "menu_settings") {
|
|
849
|
+
this.menuName = "settings"
|
|
850
|
+
await this.loadSettings()
|
|
851
|
+
}
|
|
836
852
|
else if (optionId === "menu_exit") { await this.exit(); return }
|
|
837
853
|
this.menuCtx = {}
|
|
838
854
|
this.selectedIndex = 0
|
|
@@ -902,6 +918,22 @@ export class MainScreen {
|
|
|
902
918
|
}
|
|
903
919
|
}
|
|
904
920
|
|
|
921
|
+
if (this.menuName === "settings" && optionId.startsWith("setting:")) {
|
|
922
|
+
const key = optionId.split(":", 2)[1]
|
|
923
|
+
const current = this.state.settings?.find(s => s.key === key)
|
|
924
|
+
const newValue = await this.promptInput(`${current?.label ?? key}`, key.includes("KEY"))
|
|
925
|
+
if (newValue === null) return
|
|
926
|
+
try {
|
|
927
|
+
await this.rpc.request("settings.set", { key, value: newValue })
|
|
928
|
+
this.pushLog(`Настройка ${key} обновлена`)
|
|
929
|
+
} catch (error) {
|
|
930
|
+
this.pushLog(`Ошибка: ${String(error)}`)
|
|
931
|
+
}
|
|
932
|
+
await this.loadSettings()
|
|
933
|
+
this.render()
|
|
934
|
+
return
|
|
935
|
+
}
|
|
936
|
+
|
|
905
937
|
if (this.menuName === "mail" && optionId.startsWith("mail_pick:")) {
|
|
906
938
|
const index = Number(optionId.split(":", 2)[1])
|
|
907
939
|
if (!Number.isInteger(index) || index < 0 || index >= this.state.accounts.length) return
|