From 58e89e83c14240166474b458a263b535bf6cc97c Mon Sep 17 00:00:00 2001 From: Ivan Neplokhov Date: Wed, 21 Jan 2026 14:22:52 +0400 Subject: [PATCH] some --- src/main/index.js | 8 +- src/main/store.js | 24 ++- src/main/taskRunner.js | 21 ++- src/renderer/App.jsx | 186 ++++++++++++++++++---- src/renderer/components/ErrorBoundary.jsx | 40 +++++ src/renderer/main.jsx | 7 +- src/renderer/styles/app.css | 56 +++++++ src/renderer/tabs/AccountsTab.jsx | 4 + src/renderer/tabs/LogsTab.jsx | 5 + 9 files changed, 307 insertions(+), 44 deletions(-) create mode 100644 src/renderer/components/ErrorBoundary.jsx diff --git a/src/main/index.js b/src/main/index.js index 4ff7c3e..8acbad0 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -454,6 +454,9 @@ ipcMain.handle("tasks:save", (_event, payload) => { ]; } if (existing.history_limit !== payload.task.historyLimit) changes.historyLimit = [existing.history_limit, payload.task.historyLimit]; + if (existing.max_invites_per_cycle !== Number(payload.task.maxInvitesPerCycle || 0)) { + changes.maxInvitesPerCycle = [existing.max_invites_per_cycle, Number(payload.task.maxInvitesPerCycle || 0)]; + } if (existing.allow_start_without_invite_rights !== (payload.task.allowStartWithoutInviteRights ? 1 : 0)) { changes.allowStartWithoutInviteRights = [Boolean(existing.allow_start_without_invite_rights), Boolean(payload.task.allowStartWithoutInviteRights)]; } @@ -805,10 +808,13 @@ ipcMain.handle("logs:export", async (_event, taskId) => { startedAt: log.startedAt, finishedAt: log.finishedAt, invitedCount: log.invitedCount, + cycleLimit: log.meta && log.meta.cycleLimit ? log.meta.cycleLimit : "", + queueCount: log.meta && log.meta.queueCount != null ? log.meta.queueCount : "", + batchSize: log.meta && log.meta.batchSize != null ? log.meta.batchSize : "", successIds: JSON.stringify(log.successIds || []), errors: JSON.stringify(log.errors || []) })); - const csv = toCsv(logs, ["taskId", "startedAt", "finishedAt", "invitedCount", "successIds", "errors"]); + const csv = toCsv(logs, ["taskId", "startedAt", "finishedAt", "invitedCount", "cycleLimit", "queueCount", "batchSize", "successIds", "errors"]); fs.writeFileSync(filePath, csv, "utf8"); return { ok: true, filePath }; }); diff --git a/src/main/store.js b/src/main/store.js index 8735fdf..3ea4477 100644 --- a/src/main/store.js +++ b/src/main/store.js @@ -73,7 +73,8 @@ function initStore(userDataPath) { finished_at TEXT NOT NULL, invited_count INTEGER NOT NULL, success_ids TEXT NOT NULL, - error_summary TEXT NOT NULL + error_summary TEXT NOT NULL, + meta TEXT NOT NULL DEFAULT '' ); CREATE TABLE IF NOT EXISTS account_events ( @@ -140,6 +141,7 @@ function initStore(userDataPath) { max_interval_minutes INTEGER NOT NULL, daily_limit INTEGER NOT NULL, history_limit INTEGER NOT NULL, + max_invites_per_cycle INTEGER NOT NULL DEFAULT 20, max_competitor_bots INTEGER NOT NULL, max_our_bots INTEGER NOT NULL, random_accounts INTEGER NOT NULL DEFAULT 0, @@ -208,6 +210,8 @@ function initStore(userDataPath) { ensureColumn("invites", "user_access_hash", "TEXT DEFAULT ''"); ensureColumn("invites", "confirmed", "INTEGER NOT NULL DEFAULT 1"); ensureColumn("invites", "confirm_error", "TEXT NOT NULL DEFAULT ''"); + ensureColumn("logs", "meta", "TEXT NOT NULL DEFAULT ''"); + ensureColumn("tasks", "max_invites_per_cycle", "INTEGER NOT NULL DEFAULT 20"); ensureColumn("fallback_queue", "username", "TEXT DEFAULT ''"); ensureColumn("fallback_queue", "source_chat", "TEXT DEFAULT ''"); ensureColumn("fallback_queue", "target_chat", "TEXT DEFAULT ''"); @@ -566,7 +570,7 @@ function initStore(userDataPath) { db.prepare(` UPDATE tasks SET name = ?, our_group = ?, min_interval_minutes = ?, max_interval_minutes = ?, daily_limit = ?, - history_limit = ?, max_competitor_bots = ?, max_our_bots = ?, random_accounts = ?, multi_accounts_per_run = ?, + history_limit = ?, max_invites_per_cycle = ?, max_competitor_bots = ?, max_our_bots = ?, random_accounts = ?, multi_accounts_per_run = ?, retry_on_fail = ?, auto_join_competitors = ?, auto_join_our_group = ?, separate_bot_roles = ?, require_same_bot_in_both = ?, stop_on_blocked = ?, stop_blocked_percent = ?, notes = ?, enabled = ?, allow_start_without_invite_rights = ?, parse_participants = ?, invite_via_admins = ?, invite_admin_master_id = ?, @@ -580,6 +584,7 @@ function initStore(userDataPath) { task.maxIntervalMinutes, task.dailyLimit, task.historyLimit, + task.maxInvitesPerCycle || 20, task.maxCompetitorBots, task.maxOurBots, task.randomAccounts ? 1 : 0, @@ -612,12 +617,12 @@ function initStore(userDataPath) { const result = db.prepare(` INSERT INTO tasks (name, our_group, min_interval_minutes, max_interval_minutes, daily_limit, history_limit, - max_competitor_bots, max_our_bots, random_accounts, multi_accounts_per_run, retry_on_fail, auto_join_competitors, + max_invites_per_cycle, max_competitor_bots, max_our_bots, random_accounts, multi_accounts_per_run, retry_on_fail, auto_join_competitors, auto_join_our_group, separate_bot_roles, require_same_bot_in_both, stop_on_blocked, stop_blocked_percent, notes, enabled, allow_start_without_invite_rights, parse_participants, invite_via_admins, invite_admin_master_id, invite_admin_allow_flood, warmup_enabled, warmup_start_limit, warmup_daily_increase, cycle_competitors, competitor_cursor, invite_link_on_fail, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).run( task.name, task.ourGroup, @@ -625,6 +630,7 @@ function initStore(userDataPath) { task.maxIntervalMinutes, task.dailyLimit, task.historyLimit, + task.maxInvitesPerCycle || 20, task.maxCompetitorBots, task.maxOurBots, task.randomAccounts ? 1 : 0, @@ -894,15 +900,16 @@ function initStore(userDataPath) { function addLog(entry) { db.prepare(` - INSERT INTO logs (task_id, started_at, finished_at, invited_count, success_ids, error_summary) - VALUES (?, ?, ?, ?, ?, ?) + INSERT INTO logs (task_id, started_at, finished_at, invited_count, success_ids, error_summary, meta) + VALUES (?, ?, ?, ?, ?, ?, ?) `).run( entry.taskId || 0, entry.startedAt, entry.finishedAt, entry.invitedCount, JSON.stringify(entry.successIds || []), - JSON.stringify(entry.errors || []) + JSON.stringify(entry.errors || []), + JSON.stringify(entry.meta || {}) ); } @@ -920,7 +927,8 @@ function initStore(userDataPath) { finishedAt: row.finished_at, invitedCount: row.invited_count, successIds: JSON.parse(row.success_ids || "[]"), - errors: JSON.parse(row.error_summary || "[]") + errors: JSON.parse(row.error_summary || "[]"), + meta: JSON.parse(row.meta || "{}") })); } diff --git a/src/main/taskRunner.js b/src/main/taskRunner.js index 4feba25..91db7e3 100644 --- a/src/main/taskRunner.js +++ b/src/main/taskRunner.js @@ -86,6 +86,8 @@ class TaskRunner { const accountMap = new Map( this.store.listAccounts().map((account) => [account.id, account]) ); + const perCycleLimit = Math.max(1, Number(this.task.max_invites_per_cycle || this.task.maxInvitesPerCycle || 20)); + this.cycleMeta = { cycleLimit: perCycleLimit, queueCount: 0, batchSize: 0 }; try { const settings = this.store.getSettings(); @@ -143,8 +145,22 @@ class TaskRunner { errors.push("Daily limit reached"); } else { const remaining = dailyLimit - alreadyInvited; - const batchSize = Math.min(20, remaining); + const perCycle = perCycleLimit; + const batchSize = Math.min(perCycle, remaining); + const queueCount = this.store.getPendingCount(this.task.id); const pending = this.store.getPendingInvites(this.task.id, batchSize); + this.cycleMeta = { cycleLimit: perCycle, queueCount, batchSize }; + if (inviteAccounts.length) { + const eventAccountId = inviteAccounts[0] || 0; + const eventAccount = accountMap.get(eventAccountId); + const phone = eventAccount ? eventAccount.phone : ""; + this.store.addAccountEvent( + eventAccountId, + phone, + "cycle_batch", + `лимит цикла: ${perCycle}, очередь: ${queueCount}, взято: ${pending.length}` + ); + } const accountMap = new Map(this.store.listAccounts().map((account) => [account.id, account])); if (!inviteAccounts.length && pending.length) { errors.push("No available accounts under limits"); @@ -347,7 +363,8 @@ class TaskRunner { finishedAt, invitedCount, successIds, - errors + errors, + meta: { cycleLimit: perCycleLimit, ...(this.cycleMeta || {}) } }); this._scheduleNext(); diff --git a/src/renderer/App.jsx b/src/renderer/App.jsx index 2aa4947..1fada09 100644 --- a/src/renderer/App.jsx +++ b/src/renderer/App.jsx @@ -26,6 +26,7 @@ const emptySettings = { maxIntervalMinutes: 10, dailyLimit: 100, historyLimit: 100, + maxInvitesPerCycle: 20, maxCompetitorBots: 1, maxOurBots: 1, randomAccounts: false, @@ -61,6 +62,7 @@ const emptySettings = { maxIntervalMinutes: Number(row.max_interval_minutes || 10), dailyLimit: Number(row.daily_limit || 100), historyLimit: Number(row.history_limit || 200), + maxInvitesPerCycle: Number(row.max_invites_per_cycle || 20), maxCompetitorBots: Number(row.max_competitor_bots || 1), maxOurBots: Number(row.max_our_bots || 1), randomAccounts: Boolean(row.random_accounts), @@ -179,6 +181,7 @@ export default function App() { const [taskFilter, setTaskFilter] = useState("all"); const [notificationFilter, setNotificationFilter] = useState("all"); const [infoOpen, setInfoOpen] = useState(false); + const [infoTab, setInfoTab] = useState("usage"); const [activeTab, setActiveTab] = useState("task"); const [logsTab, setLogsTab] = useState("logs"); const [logSearch, setLogSearch] = useState(""); @@ -233,6 +236,31 @@ export default function App() { const username = account.username ? `@${account.username}` : ""; return username ? `${base} (${username})` : base; }; + const copyToClipboard = async (text) => { + if (!text) return false; + try { + if (navigator && navigator.clipboard && navigator.clipboard.writeText) { + await navigator.clipboard.writeText(text); + return true; + } + } catch (error) { + // ignore and fallback + } + try { + const el = document.createElement("textarea"); + el.value = text; + el.setAttribute("readonly", ""); + el.style.position = "absolute"; + el.style.left = "-9999px"; + document.body.appendChild(el); + el.select(); + const ok = document.execCommand("copy"); + document.body.removeChild(el); + return ok; + } catch (error) { + return false; + } + }; const accountStatsMap = useMemo(() => { const map = new Map(); (accountStats || []).forEach((item) => { @@ -1905,27 +1933,86 @@ export default function App() {

Как пользоваться

-
    -
  1. Создайте задачу: название, наша группа и группы конкурентов.
  2. -
  3. Выберите аккаунты для задачи и сохраните.
  4. -
  5. Нажмите “Собрать историю”, чтобы добавить авторов из последних сообщений.
  6. -
  7. Нажмите “Запустить”, чтобы отслеживать новые сообщения и приглашать по расписанию.
  8. -
  9. Создавайте несколько задач для разных групп и контролируйте их по списку.
  10. -
-

- “Собрать историю” добавляет в очередь пользователей, которые писали ранее. Без этого будут учитываться только новые сообщения. -

-
- Важные ограничения и решения: -
1) AUTH_KEY_DUPLICATED: выйти из аккаунта на других устройствах и пересоздать tdata.
-
2) CHAT_ADMIN_REQUIRED: приглашающий аккаунт должен быть админом с правом “добавлять участников”.
-
3) USER_ID_INVALID: скрытые участники, анонимы, каналы — инвайт возможен только по username.
-
4) USER_NOT_MUTUAL_CONTACT: часто это спам‑защита или повторное добавление после выхода; помогает взаимный контакт или инвайт‑ссылка.
-
5) Инвайт через админов: можно обходить часть лимитов, временно выдавая права “Приглашать”.
-
6) Инвайт в чат с флудом: используется временная выдача прав между аккаунтами.
-
7) Нет доступа к конкурентам: используйте валидные invite‑ссылки или публичные группы.
-
8) FLOOD/PEER_FLOOD: снижайте лимиты, увеличивайте интервалы, распределяйте нагрузку.
+
+ + + +
+ {infoTab === "usage" && ( + <> +
    +
  1. Создайте задачу: название, наша группа и группы конкурентов.
  2. +
  3. Импортируйте аккаунты (tdata) и назначьте роли для задачи.
  4. +
  5. Нажмите “Собрать историю”, чтобы добавить авторов из последних сообщений.
  6. +
  7. Нажмите “Запустить”, чтобы отслеживать новые сообщения и приглашать по расписанию.
  8. +
  9. Следите за статусом, логами, событиями и очередью.
  10. +
+

+ “Собрать историю” добавляет в очередь авторов старых сообщений. Без этого учитываются только новые сообщения. +

+ + )} + {infoTab === "features" && ( +
+ Функции и режимы: +
1) Мониторинг: отслеживает новые сообщения в чатах конкурентов и добавляет авторов в очередь.
+
2) Инвайт по расписанию: приглашает с интервалом и дневным лимитом.
+
3) Инвайт через админов: временно выдает право “Приглашать”, затем снимает.
+
4) Инвайт в чаты с флудом: распределяет инвайт через цепочку выдачи прав.
+
5) Циклический обход конкурентов: переключает мониторинг по списку групп.
+
6) Парсинг участников: пытается получить список участников для закрытых чатов.
+
7) Прогрев лимита: плавно увеличивает дневной лимит по дням.
+
8) Fallback‑лист: собирает проблемные инвайты и предлагает маршруты.
+
+ )} + {infoTab === "strategies" && ( +
+ Стратегии инвайта: +
1) access_hash из сообщения.
+
2) Резолв через участников/источник.
+
3) Инвайт по username (если доступен).
+
4) Инвайт через админов (если включен).
+
5) Отправка инвайт‑ссылки (если включено).
+
После успешного инвайта выполняется проверка фактического вступления.
+
+ )} + {infoTab === "limits" && ( +
+ Особенности Telegram и ошибки: +
1) AUTH_KEY_DUPLICATED: tdata уже используется — выйдите из аккаунта на других устройствах и пересоберите tdata.
+
2) CHAT_ADMIN_REQUIRED: аккаунт должен быть админом с правом “добавлять участников”.
+
3) USER_ID_INVALID: скрытые/анонимные авторы, удаленные аккаунты — инвайт возможен только по username.
+
4) USER_NOT_MUTUAL_CONTACT: ограничения Telegram/приватность пользователя — помогает инвайт‑ссылка или другой аккаунт.
+
5) USER_PRIVACY_RESTRICTED: пользователь запретил инвайты в чаты.
+
6) FLOOD/PEER_FLOOD: снизить лимиты, увеличить интервалы, распределить нагрузку.
+
7) CHANNEL_PRIVATE/INVITE_HASH_INVALID: ссылка недействительна или чат приватный.
+
+ )}
)} @@ -2390,18 +2477,37 @@ export default function App() {
+