izteamslots 1.2.0 → 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 CHANGED
@@ -1,3 +1,10 @@
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
+
1
8
  # [1.2.0](https://github.com/izzzzzi/izTeamSlots/compare/v1.1.6...v1.2.0) (2026-03-06)
2
9
 
3
10
 
@@ -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})
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "izteamslots",
3
- "version": "1.2.0",
3
+ "version": "1.3.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"
@@ -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-4: выбор Enter: открыть r: обновить q: выход"
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",
@@ -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