From ac63ce91aa729c3bb73f8407b609abda802c75bf Mon Sep 17 00:00:00 2001 From: Ivan Neplokhov Date: Wed, 28 Jan 2026 01:11:14 +0400 Subject: [PATCH] some --- src/main/index.js | 7 ++ src/main/preload.js | 1 + src/main/store.js | 16 ++- src/main/taskRunner.js | 41 +++++++- src/main/telegram.js | 189 +++++++++++++++++++++++++----------- src/renderer/App.jsx | 64 ++++++++++-- src/renderer/styles/app.css | 8 +- 7 files changed, 260 insertions(+), 66 deletions(-) diff --git a/src/main/index.js b/src/main/index.js index 496e119..94d6c24 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -99,6 +99,7 @@ const startTaskWithChecks = async (id) => { runner.task = task; } store.setTaskStopReason(id, ""); + store.addAccountEvent(0, "", "task_start", `задача ${id}: запуск`); await runner.start(); const warnings = []; if (accessCheck && accessCheck.ok) { @@ -527,6 +528,7 @@ ipcMain.handle("tasks:stop", (_event, id) => { } store.setTaskStopReason(id, "Остановлено пользователем"); store.addTaskAudit(id, "stop", "Остановлено пользователем"); + store.addAccountEvent(0, "", "task_stop", `задача ${id}: остановлена пользователем`); return { ok: true }; }); ipcMain.handle("tasks:accountAssignments", () => { @@ -1045,6 +1047,11 @@ ipcMain.handle("fallback:clear", (_event, taskId) => { ipcMain.handle("accounts:events", async (_event, limit) => { return store.listAccountEvents(limit || 200); }); +ipcMain.handle("accounts:eventAdd", (_event, payload) => { + if (!payload) return { ok: false }; + store.addAccountEvent(payload.accountId || 0, payload.phone || "", payload.action || "custom", payload.details || ""); + return { ok: true }; +}); ipcMain.handle("accounts:events:clear", async () => { store.clearAccountEvents(); diff --git a/src/main/preload.js b/src/main/preload.js index 62a0342..dd26b4e 100644 --- a/src/main/preload.js +++ b/src/main/preload.js @@ -6,6 +6,7 @@ contextBridge.exposeInMainWorld("api", { listAccounts: () => ipcRenderer.invoke("accounts:list"), resetAccountCooldown: (accountId) => ipcRenderer.invoke("accounts:resetCooldown", accountId), listAccountEvents: (limit) => ipcRenderer.invoke("accounts:events", limit), + addAccountEvent: (payload) => ipcRenderer.invoke("accounts:eventAdd", payload), clearAccountEvents: () => ipcRenderer.invoke("accounts:events:clear"), deleteAccount: (accountId) => ipcRenderer.invoke("accounts:delete", accountId), resetSessions: () => ipcRenderer.invoke("sessions:reset"), diff --git a/src/main/store.js b/src/main/store.js index 65396f5..c8ea3de 100644 --- a/src/main/store.js +++ b/src/main/store.js @@ -163,6 +163,7 @@ function initStore(userDataPath) { invite_admin_anonymous INTEGER NOT NULL DEFAULT 1, separate_confirm_roles INTEGER NOT NULL DEFAULT 0, max_confirm_bots INTEGER NOT NULL DEFAULT 1, + use_watcher_invite_no_username INTEGER NOT NULL DEFAULT 1, warmup_enabled INTEGER NOT NULL DEFAULT 1, warmup_start_limit INTEGER NOT NULL DEFAULT 3, warmup_daily_increase INTEGER NOT NULL DEFAULT 2, @@ -258,6 +259,7 @@ function initStore(userDataPath) { ensureColumn("tasks", "invite_admin_anonymous", "INTEGER NOT NULL DEFAULT 1"); ensureColumn("tasks", "separate_confirm_roles", "INTEGER NOT NULL DEFAULT 0"); ensureColumn("tasks", "max_confirm_bots", "INTEGER NOT NULL DEFAULT 1"); + ensureColumn("tasks", "use_watcher_invite_no_username", "INTEGER NOT NULL DEFAULT 1"); ensureColumn("task_accounts", "role_confirm", "INTEGER NOT NULL DEFAULT 1"); ensureColumn("tasks", "warmup_enabled", "INTEGER NOT NULL DEFAULT 0"); ensureColumn("tasks", "warmup_start_limit", "INTEGER NOT NULL DEFAULT 3"); @@ -469,6 +471,14 @@ function initStore(userDataPath) { function enqueueInvite(taskId, userId, username, sourceChat, accessHash, watcherAccountId) { const now = dayjs().toISOString(); try { + if (taskId) { + const existing = db.prepare( + "SELECT status FROM invites WHERE task_id = ? AND user_id = ? ORDER BY invited_at DESC LIMIT 1" + ).get(taskId || 0, userId); + if (existing && existing.status === "success") { + return false; + } + } const result = db.prepare(` INSERT OR IGNORE INTO invite_queue (task_id, user_id, username, user_access_hash, watcher_account_id, source_chat, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, ?) @@ -591,6 +601,7 @@ function initStore(userDataPath) { 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 = ?, invite_admin_anonymous = ?, separate_confirm_roles = ?, max_confirm_bots = ?, + use_watcher_invite_no_username = ?, warmup_enabled = ?, warmup_start_limit = ?, warmup_daily_increase = ?, cycle_competitors = ?, competitor_cursor = ?, invite_link_on_fail = ?, updated_at = ? WHERE id = ? @@ -623,6 +634,7 @@ function initStore(userDataPath) { task.inviteAdminAnonymous ? 1 : 0, task.separateConfirmRoles ? 1 : 0, task.maxConfirmBots || 1, + task.useWatcherInviteNoUsername ? 1 : 0, task.warmupEnabled ? 1 : 0, task.warmupStartLimit || 3, task.warmupDailyIncrease || 2, @@ -641,9 +653,10 @@ function initStore(userDataPath) { 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, invite_admin_anonymous, separate_confirm_roles, max_confirm_bots, + use_watcher_invite_no_username, 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, @@ -673,6 +686,7 @@ function initStore(userDataPath) { task.inviteAdminAnonymous ? 1 : 0, task.separateConfirmRoles ? 1 : 0, task.maxConfirmBots || 1, + task.useWatcherInviteNoUsername ? 1 : 0, task.warmupEnabled ? 1 : 0, task.warmupStartLimit || 3, task.warmupDailyIncrease || 2, diff --git a/src/main/taskRunner.js b/src/main/taskRunner.js index 257f463..c1ca6f0 100644 --- a/src/main/taskRunner.js +++ b/src/main/taskRunner.js @@ -131,6 +131,16 @@ class TaskRunner { const entry = this.telegram.pickInviteAccount(inviteAccounts, Boolean(this.task.random_accounts)); inviteAccounts = entry ? [entry.account.id] : []; this.nextInviteAccountId = entry ? entry.account.id : 0; + if (entry && entry.account) { + const label = entry.account.phone || entry.account.username || entry.account.id; + const poolLabel = inviteAccounts.length ? inviteAccounts.join(",") : "—"; + this.store.addAccountEvent( + entry.account.id, + entry.account.phone || "", + "invite_pick", + `выбран: ${label}; режим: ${this.task.random_accounts ? "случайный" : "по очереди"}` + ); + } } else if (inviteAccounts.length) { this.nextInviteAccountId = inviteAccounts[0]; } @@ -150,6 +160,12 @@ class TaskRunner { const alreadyInvited = this.store.countInvitesToday(this.task.id); if (alreadyInvited >= dailyLimit) { errors.push("Daily limit reached"); + this.store.addAccountEvent( + 0, + "", + "invite_skipped", + `задача ${this.task.id}: дневной лимит ${alreadyInvited}/${dailyLimit}` + ); } else { const remaining = dailyLimit - alreadyInvited; const perCycle = perCycleLimit; @@ -170,6 +186,20 @@ class TaskRunner { } if (!inviteAccounts.length && pending.length) { errors.push("No available accounts under limits"); + this.store.addAccountEvent( + 0, + "", + "invite_skipped", + `задача ${this.task.id}: нет аккаунтов с ролью инвайта` + ); + } + if (!pending.length) { + this.store.addAccountEvent( + 0, + "", + "invite_skipped", + `задача ${this.task.id}: очередь пуста` + ); } const fallbackRoute = (error, confirmed) => { @@ -197,9 +227,18 @@ class TaskRunner { for (const item of pending) { if (item.attempts >= 2 && this.task.retry_on_fail) { this.store.markInviteStatus(item.id, "failed"); + this.store.addAccountEvent( + 0, + "", + "invite_skipped", + `задача ${this.task.id}: превышен лимит повторов для ${item.user_id}` + ); continue; } - const accountsForInvite = inviteAccounts; + let accountsForInvite = inviteAccounts; + if (!item.username && this.task.use_watcher_invite_no_username && item.watcher_account_id) { + accountsForInvite = [item.watcher_account_id]; + } const watcherAccount = accountMap.get(item.watcher_account_id || 0); const result = await this.telegram.inviteUserForTask(this.task, item.user_id, accountsForInvite, { randomize: Boolean(this.task.random_accounts), diff --git a/src/main/telegram.js b/src/main/telegram.js index da8f360..5f15b48 100644 --- a/src/main/telegram.js +++ b/src/main/telegram.js @@ -60,6 +60,7 @@ class TelegramManager { const inviteUsers = rights ? Boolean(rights.inviteUsers || rights.addUsers) : false; const addAdmins = rights ? Boolean(rights.addAdmins) : false; lines.push(`Роль: ${isCreator ? "creator" : isAdmin ? "admin" : "member"}`); + lines.push(`Права: inviteUsers=${inviteUsers}; addUsers=${rights ? Boolean(rights.addUsers) : false}; addAdmins=${addAdmins}`); lines.push(`inviteUsers: ${inviteUsers}`); lines.push(`addAdmins: ${addAdmins}`); } catch (error) { @@ -101,6 +102,14 @@ class TelegramManager { adminRights: rights, rank: "invite" })); + if (account) { + this.store.addAccountEvent( + account.id, + account.phone || "", + "temp_admin_granted", + `${targetEntity && targetEntity.title ? targetEntity.title : "цель"}` + ); + } } async _revokeTempInviteAdmin(masterClient, targetEntity, account) { @@ -648,6 +657,10 @@ class TelegramManager { } finalResult = retry; } + if (finalResult.confirmed !== true && !finalResult.detail) { + const label = directLabel || "проверка этим аккаунтом"; + finalResult.detail = buildConfirmDetail("CONFIRM_UNKNOWN", "результат проверки не определен", label); + } return { ...finalResult, attempts }; }; const attemptInvite = async (user) => { @@ -737,19 +750,40 @@ class TelegramManager { const sourceChat = options.sourceChat || ""; const attempts = []; let user = null; - if (accessHash) { + const resolveEvents = []; + if (providedUsername) { + const username = providedUsername.startsWith("@") ? providedUsername : `@${providedUsername}`; + try { + user = await client.getEntity(username); + attempts.push({ strategy: "username", ok: true, detail: username }); + resolveEvents.push(`username: ok (${username})`); + } catch (error) { + user = null; + attempts.push({ strategy: "username", ok: false, detail: "resolve failed" }); + resolveEvents.push("username: fail (resolve failed)"); + } + } else { + attempts.push({ strategy: "username", ok: false, detail: "username not provided" }); + resolveEvents.push("username: skip (no username)"); + } + if (!user && accessHash) { try { user = new Api.InputUser({ userId: BigInt(userId), accessHash: BigInt(accessHash) }); attempts.push({ strategy: "access_hash", ok: true, detail: "from message" }); + resolveEvents.push("access_hash: ok (from message)"); } catch (error) { user = null; attempts.push({ strategy: "access_hash", ok: false, detail: "invalid access_hash" }); + resolveEvents.push("access_hash: fail (invalid access_hash)"); } } else { - attempts.push({ strategy: "access_hash", ok: false, detail: "not provided" }); + if (!accessHash) { + attempts.push({ strategy: "access_hash", ok: false, detail: "not provided" }); + resolveEvents.push("access_hash: fail (not provided)"); + } } if (!user && sourceChat) { const resolved = await this._resolveUserFromSource(client, sourceChat, userId); @@ -760,32 +794,34 @@ class TelegramManager { accessHash: BigInt(resolved.accessHash) }); attempts.push({ strategy: "participants", ok: true, detail: resolved.detail || "from participants" }); + resolveEvents.push(`participants: ok (${resolved.detail || "from participants"})`); } catch (error) { user = null; attempts.push({ strategy: "participants", ok: false, detail: "invalid access_hash" }); + resolveEvents.push("participants: fail (invalid access_hash)"); } } else { attempts.push({ strategy: "participants", ok: false, detail: resolved ? resolved.detail : "no result" }); + resolveEvents.push(`participants: fail (${resolved ? resolved.detail : "no result"})`); } } else if (!user && !sourceChat) { attempts.push({ strategy: "participants", ok: false, detail: "source chat not provided" }); + resolveEvents.push("participants: skip (no source chat)"); } - if (!user && providedUsername) { - const username = providedUsername.startsWith("@") ? providedUsername : `@${providedUsername}`; - try { - user = await client.getEntity(username); - attempts.push({ strategy: "username", ok: true, detail: username }); - } catch (error) { - user = null; - attempts.push({ strategy: "username", ok: false, detail: "resolve failed" }); - } - } else if (!user && !providedUsername) { - attempts.push({ strategy: "username", ok: false, detail: "username not provided" }); - } + // username already attempted above if (!user) { const resolvedUser = await client.getEntity(userId); user = await client.getInputEntity(resolvedUser); attempts.push({ strategy: "entity", ok: true, detail: "getEntity(userId)" }); + resolveEvents.push("entity: ok (getEntity(userId))"); + } + if (account) { + this.store.addAccountEvent( + account.id, + account.phone || "", + "resolve_user", + resolveEvents.join(" | ") + ); } return { user, attempts }; }; @@ -806,6 +842,12 @@ class TelegramManager { } else { targetType = targetEntity && targetEntity.className ? targetEntity.className : ""; } + this.store.addAccountEvent( + account.id, + account.phone || "", + "invite_admin_path", + `invite_via_admins=${task.invite_via_admins ? "on" : "off"}; targetType=${targetType || "unknown"}` + ); const resolved = await resolveInputUser(); lastAttempts = resolved.attempts || []; const user = resolved.user; @@ -905,7 +947,19 @@ class TelegramManager { lastAttempts.push({ strategy: "admin_invite", ok: false, detail: adminText }); } } + this.store.addAccountEvent( + account.id, + account.phone || "", + "invite_attempt", + `${userId} -> ${task.our_group || "цель"}` + ); await attemptInvite(user); + this.store.addAccountEvent( + account.id, + account.phone || "", + "invite_sent", + `${userId} -> ${task.our_group || "цель"}` + ); const confirm = await confirmMembershipWithFallback(user, entry); if (confirm.confirmed !== true && !confirm.detail) { const label = formatAccountSource("", entry) || "проверка этим аккаунтом"; @@ -914,6 +968,12 @@ class TelegramManager { if (confirm.attempts && confirm.attempts.length) { lastAttempts.push(...confirm.attempts); } + this.store.addAccountEvent( + account.id, + account.phone || "", + confirm.confirmed === true ? "confirm_ok" : "confirm_unconfirmed", + `${userId} -> ${confirm.detail || "не подтверждено"}` + ); this.store.updateAccountStatus(account.id, "ok", ""); const last = lastAttempts.filter((item) => item.ok).slice(-1)[0]; @@ -929,6 +989,12 @@ class TelegramManager { }; } catch (error) { const errorText = error.errorMessage || error.message || String(error); + this.store.addAccountEvent( + account.id, + account.phone || "", + "invite_failed", + `${userId} -> ${task.our_group || "цель"} | ${errorText}` + ); if (errorText.includes("CHAT_WRITE_FORBIDDEN")) { try { const reason = await explainWriteForbidden(); @@ -999,50 +1065,15 @@ class TelegramManager { ok: false, detail: `username=${options.username || "—"}; hash=${options.userAccessHash || "—"}; source=${options.sourceChat || "—"}` }); - const username = options.username ? (options.username.startsWith("@") ? options.username : `@${options.username}`) : ""; - try { - let retryUser = null; - if (!retryUser && options.sourceChat) { - const resolved = await this._resolveUserFromSource(client, options.sourceChat, userId); - if (resolved && resolved.accessHash) { - retryUser = new Api.InputUser({ - userId: BigInt(userId), - accessHash: BigInt(resolved.accessHash) - }); - } - } - if (!retryUser && username) { - try { - retryUser = await client.getEntity(username); - } catch (resolveError) { - retryUser = null; - } - } - if (!retryUser) { - const resolvedUser = await client.getEntity(userId); - retryUser = await client.getInputEntity(resolvedUser); - } - await attemptInvite(retryUser); - this.store.updateAccountStatus(account.id, "ok", ""); - return { ok: true, accountId: account.id, accountPhone: account.phone || "" }; - } catch (retryError) { - const retryText = retryError.errorMessage || retryError.message || String(retryError); - this.store.addAccountEvent( - account.id, - account.phone || "", - "invite_user_invalid", - `USER_ID_INVALID -> retry failed (${retryText}); user=${userId}; username=${options.username || "—"}; hash=${options.userAccessHash || "—"}` - ); - fallbackMeta = JSON.stringify([ - { strategy: "retry", ok: false, detail: retryText } - ]); - } this.store.addAccountEvent( account.id, account.phone || "", "invite_user_invalid", `USER_ID_INVALID; user=${userId}; username=${options.username || "—"}; hash=${options.userAccessHash || "—"}` ); + fallbackMeta = JSON.stringify([ + { strategy: "retry", ok: false, detail: "skip retry: user invalid" } + ]); } if (errorText.includes("FLOOD") || errorText.includes("PEER_FLOOD")) { this._applyFloodCooldown(account, errorText); @@ -1582,9 +1613,21 @@ class TelegramManager { adminRights: rights, rank: "invite" })); + this.store.addAccountEvent( + masterAccountId, + masterAccount.phone || "", + "admin_grant", + `${record.phone || record.username || record.id} -> ${task.our_group}` + ); results.push({ accountId, ok: true }); } catch (error) { const errorText = error.errorMessage || error.message || String(error); + this.store.addAccountEvent( + masterAccountId, + masterAccount.phone || "", + "admin_grant_failed", + `${record.phone || record.username || record.id} -> ${task.our_group} | ${errorText}` + ); results.push({ accountId, ok: false, reason: errorText }); } } @@ -1592,7 +1635,18 @@ class TelegramManager { } async _autoJoinGroups(client, groups, enabled, account) { - if (!enabled) return; + if (!enabled) { + if (account && (groups || []).length) { + const list = (groups || []).filter(Boolean).join(", "); + this.store.addAccountEvent( + account.id, + account.phone || "", + "auto_join_skipped", + `автовступление выключено: ${list || "без групп"}` + ); + } + return; + } const settings = this.store.getSettings(); let maxGroups = Number(account && account.max_groups != null ? account.max_groups : settings.accountMaxGroups); if (!Number.isFinite(maxGroups) || maxGroups <= 0) { @@ -1630,14 +1684,39 @@ class TelegramManager { if (this._isInviteLink(group)) { const hash = this._extractInviteHash(group); if (hash) { - await client.invoke(new Api.messages.ImportChatInvite({ hash })); + const result = await client.invoke(new Api.messages.ImportChatInvite({ hash })); + if (account) { + const className = result && result.className ? result.className : ""; + if (className.includes("ChatInviteAlready")) { + this.store.addAccountEvent(account.id, account.phone || "", "auto_join_already", group); + } else if (className.includes("ChatInvite") || className.includes("ChatInvitePeek")) { + this.store.addAccountEvent(account.id, account.phone || "", "auto_join_request", group); + } else { + this.store.addAccountEvent(account.id, account.phone || "", "auto_join_ok", group); + } + } } } else { const entity = await client.getEntity(group); - await client.invoke(new Api.channels.JoinChannel({ channel: entity })); + const result = await client.invoke(new Api.channels.JoinChannel({ channel: entity })); + if (account) { + const className = result && result.className ? result.className : ""; + if (className.includes("ChatInviteAlready")) { + this.store.addAccountEvent(account.id, account.phone || "", "auto_join_already", group); + } else if (className.includes("ChatInvite") || className.includes("ChatInvitePeek")) { + this.store.addAccountEvent(account.id, account.phone || "", "auto_join_request", group); + } else { + this.store.addAccountEvent(account.id, account.phone || "", "auto_join_ok", group); + } + } } } catch (error) { const errorText = error.errorMessage || error.message || String(error); + if (account) { + const isRequest = errorText.includes("INVITE_REQUEST_SENT"); + const action = isRequest ? "auto_join_request" : "auto_join_failed"; + this.store.addAccountEvent(account.id, account.phone || "", action, `${group} | ${errorText}`); + } if (account && (errorText.includes("FLOOD") || errorText.includes("PEER_FLOOD"))) { this._applyFloodCooldown(account, errorText); } diff --git a/src/renderer/App.jsx b/src/renderer/App.jsx index 4d39d68..cd8ff1a 100644 --- a/src/renderer/App.jsx +++ b/src/renderer/App.jsx @@ -43,6 +43,7 @@ const emptySettings = { inviteAdminAnonymous: true, separateConfirmRoles: false, maxConfirmBots: 1, + useWatcherInviteNoUsername: true, warmupEnabled: true, warmupStartLimit: 3, warmupDailyIncrease: 2, @@ -82,6 +83,7 @@ const emptySettings = { inviteAdminAnonymous: row.invite_admin_anonymous == null ? true : Boolean(row.invite_admin_anonymous), separateConfirmRoles: Boolean(row.separate_confirm_roles), maxConfirmBots: Number(row.max_confirm_bots || 1), + useWatcherInviteNoUsername: row.use_watcher_invite_no_username == null ? true : Boolean(row.use_watcher_invite_no_username), warmupEnabled: row.warmup_enabled == null ? true : Boolean(row.warmup_enabled), warmupStartLimit: Number(row.warmup_start_limit || 3), warmupDailyIncrease: Number(row.warmup_daily_increase || 2), @@ -188,6 +190,7 @@ export default function App() { const toastTimers = useRef(new Map()); const [notifications, setNotifications] = useState([]); const [notificationsOpen, setNotificationsOpen] = useState(false); + const notificationsModalRef = useRef(null); const [manualLoginOpen, setManualLoginOpen] = useState(false); const [taskSearch, setTaskSearch] = useState(""); const [taskFilter, setTaskFilter] = useState("all"); @@ -872,6 +875,9 @@ export default function App() { const handleClickOutside = (event) => { if (!notificationsOpen) return; if (!bellRef.current) return; + if (notificationsModalRef.current && notificationsModalRef.current.contains(event.target)) { + return; + } if (!bellRef.current.contains(event.target)) { setNotificationsOpen(false); } @@ -1350,8 +1356,14 @@ export default function App() { if (taskActionLoading) return; setTaskActionLoading(true); showNotification("Запуск...", "info"); + const withTimeout = (promise, ms) => ( + Promise.race([ + promise, + new Promise((_, reject) => setTimeout(() => reject(new Error("TIMEOUT")), ms)) + ]) + ); try { - const result = await window.api.startTaskById(selectedTaskId); + const result = await withTimeout(window.api.startTaskById(selectedTaskId), 15000); if (result && result.ok) { setTaskNotice({ text: "Запущено.", tone: "success", source }); if (result.warnings && result.warnings.length) { @@ -1362,7 +1374,9 @@ export default function App() { showNotification(result.error || "Не удалось запустить", "error"); } } catch (error) { - const message = error.message || String(error); + const message = error.message === "TIMEOUT" + ? "Запуск не ответил за 15 секунд. Проверьте логи/события и попробуйте снова." + : (error.message || String(error)); setTaskNotice({ text: message, tone: "error", source }); showNotification(message, "error"); } finally { @@ -1404,11 +1418,19 @@ export default function App() { } setTaskActionLoading(true); showNotification("Остановка...", "info"); + const withTimeout = (promise, ms) => ( + Promise.race([ + promise, + new Promise((_, reject) => setTimeout(() => reject(new Error("TIMEOUT")), ms)) + ]) + ); try { - await window.api.stopTaskById(selectedTaskId); + await withTimeout(window.api.stopTaskById(selectedTaskId), 15000); setTaskNotice({ text: "Остановлено.", tone: "success", source }); } catch (error) { - const message = error.message || String(error); + const message = error.message === "TIMEOUT" + ? "Остановка не ответила за 15 секунд. Проверьте логи/события и попробуйте снова." + : (error.message || String(error)); setTaskNotice({ text: message, tone: "error", source }); showNotification(message, "error"); } finally { @@ -1722,6 +1744,12 @@ export default function App() { taskId: selectedTaskId, accountRoles: rolePayload }); + await window.api.addAccountEvent({ + accountId: 0, + phone: "", + action: "roles_changed", + details: `задача ${selectedTaskId}: обновлены роли` + }); await loadAccountAssignments(); }; @@ -1867,11 +1895,19 @@ export default function App() { }); const signature = buildPresetSignature(nextForm, roleMap); presetSignatureRef.current = signature; + const label = type === "admin" ? "Автораспределение + Инвайт через админа" : "Автораспределение + Без админки"; setTaskForm(nextForm); setTaskAccountRoles(roleMap); setSelectedAccountIds(Object.keys(roleMap).map((id) => Number(id))); persistAccountRoles(roleMap); - const label = type === "admin" ? "Автораспределение + Инвайт через админа" : "Автораспределение + Без админки"; + if (window.api) { + window.api.addAccountEvent({ + accountId: 0, + phone: "", + action: "preset_applied", + details: `задача ${selectedTaskId}: ${label}` + }); + } setTaskNotice({ text: `Пресет: ${label}`, tone: "success", source: "task" }); setActivePreset(type); }; @@ -2292,7 +2328,7 @@ export default function App() { {notificationsOpen && (
setNotificationsOpen(false)}> -
event.stopPropagation()}> +
event.stopPropagation()}>

Уведомления