From 303755f2215a874f6d6b766dc48581b29d0f23cf Mon Sep 17 00:00:00 2001 From: Ivan Neplokhov Date: Fri, 23 Jan 2026 15:19:35 +0400 Subject: [PATCH] some --- src/main/index.js | 22 +- src/main/store.js | 13 + src/main/taskRunner.js | 19 +- src/main/telegram.js | 84 +- src/renderer/App.jsx | 1974 +++++++++++++++++---------------- src/renderer/styles/app.css | 326 +++++- src/renderer/tabs/LogsTab.jsx | 164 ++- 7 files changed, 1553 insertions(+), 1049 deletions(-) diff --git a/src/main/index.js b/src/main/index.js index 8acbad0..83415b7 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -112,9 +112,11 @@ const startTaskWithChecks = async (id) => { if (missingSessions.length) { warnings.push(`Сессии не подключены: ${missingSessions.length} аккаунт(ов).`); } - const noRights = (inviteAccess.result || []).filter((row) => !row.canInvite); - if (noRights.length) { - warnings.push(`Нет прав инвайта у ${noRights.length} аккаунт(ов).`); + if (task.invite_via_admins) { + const noRights = (inviteAccess.result || []).filter((row) => !row.canInvite); + if (noRights.length) { + warnings.push(`Нет прав инвайта у ${noRights.length} аккаунт(ов).`); + } } } if (task.invite_via_admins) { @@ -596,6 +598,7 @@ ipcMain.handle("tasks:status", (_event, id) => { const runner = taskRunners.get(id); const queueCount = store.getPendingCount(id); const dailyUsed = store.countInvitesToday(id); + const unconfirmedCount = store.countInvitesByStatus(id, "unconfirmed"); const task = store.getTask(id); const monitorInfo = telegram.getTaskMonitorInfo(id); const warnings = []; @@ -686,14 +689,16 @@ ipcMain.handle("tasks:status", (_event, id) => { const disconnected = parsed.filter((row) => row.reason === "Сессия не подключена").length; const isChannel = parsed.some((row) => row.targetType === "channel"); const checkedAt = task.task_invite_access_at || ""; - warnings.push(`Права инвайта: ${canInvite}/${total} аккаунтов могут добавлять.${checkedAt ? ` Проверка: ${formatTimestamp(checkedAt)}.` : ""}`); + if (task.invite_via_admins) { + warnings.push(`Права инвайта: ${canInvite}/${total} аккаунтов могут добавлять.${checkedAt ? ` Проверка: ${formatTimestamp(checkedAt)}.` : ""}`); + } if (disconnected) { warnings.push(`Сессия не подключена: ${disconnected} аккаунт(ов).`); } - if (isChannel) { + if (isChannel && task.invite_via_admins) { warnings.push("Цель — канал: добавлять участников могут только админы."); } - if (canInvite === 0) { + if (canInvite === 0 && task.invite_via_admins) { readiness.ok = false; readiness.reasons.push("Нет аккаунтов с правами инвайта."); } @@ -708,6 +713,7 @@ ipcMain.handle("tasks:status", (_event, id) => { running: runner ? runner.isRunning() : false, queueCount, dailyUsed, + unconfirmedCount, dailyLimit: effectiveLimit, dailyRemaining: task ? Math.max(0, Number(effectiveLimit || 0) - dailyUsed) : 0, cycleCompetitors: task ? Boolean(task.cycle_competitors) : false, @@ -808,13 +814,14 @@ ipcMain.handle("logs:export", async (_event, taskId) => { startedAt: log.startedAt, finishedAt: log.finishedAt, invitedCount: log.invitedCount, + unconfirmedCount: log.meta && log.meta.unconfirmedCount != null ? log.meta.unconfirmedCount : "", 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", "cycleLimit", "queueCount", "batchSize", "successIds", "errors"]); + const csv = toCsv(logs, ["taskId", "startedAt", "finishedAt", "invitedCount", "unconfirmedCount", "cycleLimit", "queueCount", "batchSize", "successIds", "errors"]); fs.writeFileSync(filePath, csv, "utf8"); return { ok: true, filePath }; }); @@ -860,6 +867,7 @@ ipcMain.handle("invites:exportProblems", async (_event, taskId) => { const time = invite.invitedAt ? new Date(invite.invitedAt).getTime() : 0; if (!Number.isFinite(time) || time < cutoff) return false; if (invite.status === "success") return false; + if (invite.status === "unconfirmed") return true; if (!invite.error && !invite.skippedReason) return false; return true; }).map((invite) => ({ diff --git a/src/main/store.js b/src/main/store.js index 3ea4477..5aaea1a 100644 --- a/src/main/store.js +++ b/src/main/store.js @@ -898,6 +898,18 @@ function initStore(userDataPath) { ).get(dayStart, accountId, taskId || 0).count; } + function countInvitesByStatus(taskId, status) { + if (!status) return 0; + if (taskId == null) { + return db.prepare( + "SELECT COUNT(*) as count FROM invites WHERE status = ? AND archived = 0" + ).get(status).count; + } + return db.prepare( + "SELECT COUNT(*) as count FROM invites WHERE status = ? AND task_id = ? AND archived = 0" + ).get(status, taskId || 0).count; + } + function addLog(entry) { db.prepare(` INSERT INTO logs (task_id, started_at, finished_at, invited_count, success_ids, error_summary, meta) @@ -1042,6 +1054,7 @@ function initStore(userDataPath) { recordInvite, countInvitesToday, countInvitesTodayByAccount, + countInvitesByStatus, addLog }; } diff --git a/src/main/taskRunner.js b/src/main/taskRunner.js index 53eeabc..8d9e894 100644 --- a/src/main/taskRunner.js +++ b/src/main/taskRunner.js @@ -81,6 +81,7 @@ class TaskRunner { const errors = []; const successIds = []; let invitedCount = 0; + let unconfirmedCount = 0; this.nextRunAt = ""; this.nextInviteAccountId = 0; const accountMap = new Map( @@ -204,10 +205,16 @@ class TaskRunner { sourceChat: item.source_chat }); if (result.ok) { - invitedCount += 1; - successIds.push(item.user_id); - this.store.markInviteStatus(item.id, "invited"); + const isConfirmed = result.confirmed === true; + if (isConfirmed) { + invitedCount += 1; + successIds.push(item.user_id); + } else { + unconfirmedCount += 1; + } + this.store.markInviteStatus(item.id, isConfirmed ? "invited" : "unconfirmed"); this.lastInviteAccountId = result.accountId || this.lastInviteAccountId; + const inviteStatus = isConfirmed ? "success" : "unconfirmed"; this.store.recordInvite( this.task.id, item.user_id, @@ -215,7 +222,7 @@ class TaskRunner { result.accountId, result.accountPhone, item.source_chat, - "success", + inviteStatus, "", "", "invite", @@ -226,7 +233,7 @@ class TaskRunner { result.strategyMeta, this.task.our_group, result.targetType, - result.confirmed !== false, + result.confirmed === true, result.confirmError || "" ); if (result.confirmed === false) { @@ -363,7 +370,7 @@ class TaskRunner { invitedCount, successIds, errors, - meta: { cycleLimit: perCycleLimit, ...(this.cycleMeta || {}) } + meta: { cycleLimit: perCycleLimit, unconfirmedCount, ...(this.cycleMeta || {}) } }); this._scheduleNext(); diff --git a/src/main/telegram.js b/src/main/telegram.js index 942c489..9d328a5 100644 --- a/src/main/telegram.js +++ b/src/main/telegram.js @@ -550,24 +550,64 @@ class TelegramManager { let targetEntity = null; let targetType = ""; let resolvedUser = null; - const confirmMembership = async (user) => { + const buildConfirmDetail = (code, message, sourceLabel) => { + if (!code) return message || ""; + const base = message ? `${code}: ${message}` : code; + return sourceLabel ? `${base} (${sourceLabel})` : base; + }; + const confirmMembership = async (user, confirmClient = client, sourceLabel = "") => { if (!targetEntity || targetEntity.className !== "Channel") { - return { confirmed: true, error: "" }; + return { confirmed: true, error: "", detail: "" }; } try { - await client.invoke(new Api.channels.GetParticipant({ + await confirmClient.invoke(new Api.channels.GetParticipant({ channel: targetEntity, participant: user })); - return { confirmed: true, error: "" }; + return { confirmed: true, error: "", detail: "" }; } catch (error) { const errorText = error.errorMessage || error.message || String(error); if (errorText.includes("USER_NOT_PARTICIPANT")) { - return { confirmed: false, error: "not in group" }; + return { + confirmed: false, + error: "USER_NOT_PARTICIPANT", + detail: buildConfirmDetail("USER_NOT_PARTICIPANT", "пользователь не в группе", sourceLabel) + }; } - return { confirmed: false, error: errorText }; + if (errorText.includes("CHAT_ADMIN_REQUIRED")) { + return { + confirmed: null, + error: "CHAT_ADMIN_REQUIRED", + detail: buildConfirmDetail("CHAT_ADMIN_REQUIRED", "нет прав для подтверждения участия", sourceLabel) + }; + } + return { + confirmed: null, + error: errorText, + detail: buildConfirmDetail(errorText, "ошибка подтверждения участия", sourceLabel) + }; } }; + const confirmMembershipWithFallback = async (user) => { + const attempts = []; + const direct = await confirmMembership(user, client, "проверка этим аккаунтом"); + if (direct.detail) { + attempts.push({ strategy: "confirm", ok: direct.confirmed === true, detail: direct.detail }); + } + if (direct.confirmed !== null) { + return { ...direct, attempts }; + } + const masterId = Number(task.invite_admin_master_id || 0); + const masterEntry = masterId ? this.clients.get(masterId) : null; + if (masterEntry && masterEntry.client && masterEntry.client !== client) { + const adminConfirm = await confirmMembership(user, masterEntry.client, "проверка админом"); + if (adminConfirm.detail) { + attempts.push({ strategy: "confirm_admin", ok: adminConfirm.confirmed === true, detail: adminConfirm.detail }); + } + return { ...adminConfirm, attempts }; + } + return { ...direct, attempts }; + }; const attemptInvite = async (user) => { if (!targetEntity) { throw new Error("Target group not resolved"); @@ -630,6 +670,8 @@ class TelegramManager { user = null; attempts.push({ strategy: "access_hash", ok: false, detail: "invalid access_hash" }); } + } else { + attempts.push({ strategy: "access_hash", ok: false, detail: "not provided" }); } if (!user && sourceChat) { const resolved = await this._resolveUserFromSource(client, sourceChat, userId); @@ -647,6 +689,8 @@ class TelegramManager { } else { attempts.push({ strategy: "participants", ok: false, detail: resolved ? resolved.detail : "no result" }); } + } else if (!user && !sourceChat) { + attempts.push({ strategy: "participants", ok: false, detail: "source chat not provided" }); } if (!user && providedUsername) { const username = providedUsername.startsWith("@") ? providedUsername : `@${providedUsername}`; @@ -657,6 +701,8 @@ class TelegramManager { 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" }); } if (!user) { const resolvedUser = await client.getEntity(userId); @@ -694,7 +740,10 @@ class TelegramManager { await this._grantTempInviteAdmin(masterEntry.client, targetEntity, account); lastAttempts.push({ strategy: "temp_admin", ok: true, detail: "granted" }); await attemptInvite(user); - const confirm = await confirmMembership(user); + const confirm = await confirmMembershipWithFallback(user); + if (confirm.attempts && confirm.attempts.length) { + lastAttempts.push(...confirm.attempts); + } lastAttempts.push({ strategy: "temp_admin_invite", ok: true, detail: "invite" }); this.store.updateAccountStatus(account.id, "ok", ""); return { @@ -705,7 +754,7 @@ class TelegramManager { strategyMeta: JSON.stringify(lastAttempts), targetType, confirmed: confirm.confirmed, - confirmError: confirm.error + confirmError: confirm.detail || "" }; } catch (adminError) { const adminText = adminError.errorMessage || adminError.message || String(adminError); @@ -727,7 +776,10 @@ class TelegramManager { const masterEntry = masterId ? this.clients.get(masterId) : null; const adminClient = masterEntry ? masterEntry.client : client; await attemptAdminInvite(user, adminClient); - const confirm = await confirmMembership(user); + const confirm = await confirmMembershipWithFallback(user); + if (confirm.attempts && confirm.attempts.length) { + lastAttempts.push(...confirm.attempts); + } lastAttempts.push({ strategy: "admin_invite", ok: true, detail: "editAdmin" }); this.store.updateAccountStatus(account.id, "ok", ""); return { @@ -738,7 +790,7 @@ class TelegramManager { strategyMeta: JSON.stringify(lastAttempts), targetType, confirmed: confirm.confirmed, - confirmError: confirm.error + confirmError: confirm.detail || "" }; } catch (adminError) { const adminText = adminError.errorMessage || adminError.message || String(adminError); @@ -761,7 +813,10 @@ class TelegramManager { } } await attemptInvite(user); - const confirm = await confirmMembership(user); + const confirm = await confirmMembershipWithFallback(user); + if (confirm.attempts && confirm.attempts.length) { + lastAttempts.push(...confirm.attempts); + } this.store.updateAccountStatus(account.id, "ok", ""); const last = lastAttempts.filter((item) => item.ok).slice(-1)[0]; @@ -773,7 +828,7 @@ class TelegramManager { strategyMeta: JSON.stringify(lastAttempts), targetType, confirmed: confirm.confirmed, - confirmError: confirm.error + confirmError: confirm.detail || "" }; } catch (error) { const errorText = error.errorMessage || error.message || String(error); @@ -834,6 +889,11 @@ class TelegramManager { } } if (errorText === "USER_ID_INVALID") { + lastAttempts.push({ + strategy: "user_id_invalid", + 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; diff --git a/src/renderer/App.jsx b/src/renderer/App.jsx index 1fada09..2e69dc4 100644 --- a/src/renderer/App.jsx +++ b/src/renderer/App.jsx @@ -170,6 +170,7 @@ export default function App() { const [loginId, setLoginId] = useState(""); const [loginStatus, setLoginStatus] = useState(""); const [taskNotice, setTaskNotice] = useState(null); + const [autosaveNote, setAutosaveNote] = useState(""); const [settingsNotice, setSettingsNotice] = useState(null); const [tdataNotice, setTdataNotice] = useState(null); const [toasts, setToasts] = useState([]); @@ -199,6 +200,9 @@ export default function App() { const [isVisible, setIsVisible] = useState(!document.hidden); const bellRef = useRef(null); const settingsAutosaveReady = useRef(false); + const taskAutosaveReady = useRef(false); + const taskAutosaveTimer = useRef(null); + const autosaveNoteTimer = useRef(null); const tasksPollInFlight = useRef(false); const accountsPollInFlight = useRef(false); const logsPollInFlight = useRef(false); @@ -362,6 +366,7 @@ export default function App() { }; const loadSelectedTask = async (taskId) => { + taskAutosaveReady.current = false; if (!taskId) { setTaskForm(emptyTaskForm); setCompetitorText(""); @@ -382,10 +387,14 @@ export default function App() { lastInviteAccountId: 0, pendingStats: { total: 0, withUsername: 0, withAccessHash: 0, withoutData: 0 } }); + taskAutosaveReady.current = true; return; } const details = await window.api.getTask(taskId); - if (!details) return; + if (!details) { + taskAutosaveReady.current = true; + return; + } setTaskForm(sanitizeTaskForm({ ...emptyTaskForm, ...normalizeTask(details.task) })); setCompetitorText((details.competitors || []).join("\n")); const roleMap = {}; @@ -408,6 +417,7 @@ export default function App() { setFallbackList(await window.api.listFallback({ limit: 500, taskId })); setGroupVisibility([]); setTaskStatus(await window.api.taskStatus(taskId)); + taskAutosaveReady.current = true; }; const loadBase = async () => { @@ -693,7 +703,7 @@ export default function App() { return "Пользователь не взаимный контакт для добавляющего аккаунта. Обычно это происходит, когда в группе/канале включена опция «добавлять могут только контакты» или у пользователя закрыт приём инвайтов. Решение: использовать аккаунт, который уже в контактах у пользователя, или поменять настройки группы."; } if (error === "USER_PRIVACY_RESTRICTED") { - return "Пользователь ограничил приватность и не принимает инвайты."; + return "Приглашение запрещено пользователем: приватность не позволяет добавлять в группы."; } if (error === "USER_NOT_PARTICIPANT") { return "Аккаунт не состоит в целевой группе или канал приватный."; @@ -850,6 +860,7 @@ export default function App() { if (inviteFilter === "success" && invite.status !== "success") return false; if (inviteFilter === "error" && invite.status !== "failed") return false; if (inviteFilter === "skipped" && invite.status !== "skipped") return false; + if (inviteFilter === "unconfirmed" && invite.status !== "unconfirmed") return false; const text = [ invite.invitedAt, invite.userId, @@ -860,7 +871,8 @@ export default function App() { invite.strategy, invite.strategyMeta, invite.error, - invite.skippedReason + invite.skippedReason, + invite.confirmError ] .join(" ") .toLowerCase(); @@ -924,6 +936,34 @@ export default function App() { }); return { success, failed }; }, [invites]); + const inviteStats = useMemo(() => { + const stats = { + total: invites.length, + success: 0, + failed: 0, + skipped: 0, + unconfirmed: 0 + }; + invites.forEach((invite) => { + switch (invite.status) { + case "success": + stats.success += 1; + break; + case "failed": + stats.failed += 1; + break; + case "skipped": + stats.skipped += 1; + break; + case "unconfirmed": + stats.unconfirmed += 1; + break; + default: + break; + } + }); + return stats; + }, [invites]); const logPageSize = 20; const invitePageSize = 20; @@ -1047,7 +1087,20 @@ export default function App() { return () => clearTimeout(timer); }, [settings]); + useEffect(() => { + if (!taskAutosaveReady.current) return; + if (!canSaveTask) return; + if (taskAutosaveTimer.current) { + clearTimeout(taskAutosaveTimer.current); + } + taskAutosaveTimer.current = setTimeout(() => { + saveTask("autosave", { silent: true }); + }, 800); + return () => clearTimeout(taskAutosaveTimer.current); + }, [taskForm, competitorText, taskAccountRoles, selectedAccountIds, canSaveTask]); + const createTask = () => { + taskAutosaveReady.current = false; setSelectedTaskId(null); setTaskForm(emptyTaskForm); setCompetitorText(""); @@ -1055,6 +1108,7 @@ export default function App() { setTaskAccountRoles({}); setAccessStatus([]); setMembershipStatus({}); + taskAutosaveReady.current = true; }; const selectTask = (taskId) => { @@ -1062,13 +1116,18 @@ export default function App() { setSelectedTaskId(taskId); }; - const saveTask = async (source = "editor") => { + const saveTask = async (source = "editor", options = {}) => { + const silent = Boolean(options.silent); if (!window.api) { - showNotification("Electron API недоступен. Откройте приложение в Electron.", "error"); + if (!silent) { + showNotification("Electron API недоступен. Откройте приложение в Electron.", "error"); + } return; } try { - showNotification("Сохраняем задачу...", "info"); + if (!silent) { + showNotification("Сохраняем задачу...", "info"); + } const nextForm = sanitizeTaskForm(taskForm); setTaskForm(nextForm); const validateLink = (value) => { @@ -1111,12 +1170,14 @@ export default function App() { }); setTaskAccountRoles(accountRolesMap); setSelectedAccountIds(accountIds); - if (accountIds.length) { + if (accountIds.length && !silent) { setTaskNotice({ text: `Автоназначены аккаунты: ${accountIds.length}`, tone: "success", source }); } } if (!accountIds.length) { - showNotification("Нет аккаунтов для этой задачи.", "error"); + if (!silent) { + showNotification("Нет аккаунтов для этой задачи.", "error"); + } return; } const roleEntries = Object.values(accountRolesMap); @@ -1124,11 +1185,15 @@ export default function App() { const hasMonitor = roleEntries.some((item) => item.monitor); const hasInvite = roleEntries.some((item) => item.invite); if (!hasMonitor) { - showNotification("Нужен хотя бы один аккаунт с ролью мониторинга.", "error"); + if (!silent) { + showNotification("Нужен хотя бы один аккаунт с ролью мониторинга.", "error"); + } return; } if (!hasInvite) { - showNotification("Нужен хотя бы один аккаунт с ролью инвайта.", "error"); + if (!silent) { + showNotification("Нужен хотя бы один аккаунт с ролью инвайта.", "error"); + } return; } } else { @@ -1138,7 +1203,9 @@ export default function App() { ? Math.max(1, Number(nextForm.maxCompetitorBots || 1)) + Math.max(1, Number(nextForm.maxOurBots || 1)) : 1; if (accountIds.length < requiredAccounts) { - showNotification(`Нужно минимум ${requiredAccounts} аккаунтов для выбранного режима.`, "error"); + if (!silent) { + showNotification(`Нужно минимум ${requiredAccounts} аккаунтов для выбранного режима.`, "error"); + } return; } } @@ -1154,15 +1221,29 @@ export default function App() { accountRoles }); if (result.ok) { - setTaskNotice({ text: "Задача сохранена.", tone: "success", source }); + if (!silent) { + setTaskNotice({ text: "Задача сохранена.", tone: "success", source }); + } else { + setAutosaveNote("Автосохранено"); + if (autosaveNoteTimer.current) { + clearTimeout(autosaveNoteTimer.current); + } + autosaveNoteTimer.current = setTimeout(() => { + setAutosaveNote(""); + }, 1500); + } await loadTasks(); await loadAccountAssignments(); setSelectedTaskId(result.taskId); } else { - showNotification(result.error || "Не удалось сохранить задачу", "error"); + if (!silent) { + showNotification(result.error || "Не удалось сохранить задачу", "error"); + } } } catch (error) { - showNotification(error.message || String(error), "error"); + if (!silent) { + showNotification(error.message || String(error), "error"); + } } }; @@ -1841,91 +1922,251 @@ export default function App() { return (
-
-
-

Автоматизация инвайтов

-

Парсинг сообщений и приглашения в целевые группы.

-
-
- {criticalEvents.length > 0 && ( -
- Ошибки: {criticalEvents.length} +
+
+ +
+
+

Общий обзор

+
+
+
+ + {notifications.length > 0 && ( + {notifications.length} + )} +
+ +
+
+
+
+
+
Всего задач
+
{taskSummary.total}
+
+
+
Запущено
+
{taskSummary.running}
+
+
+
Сессии
+
+ {globalStatus.connectedSessions}/{globalStatus.totalAccounts} +
+
+
+
Очередь
+
{taskSummary.queue}
+
+
+
Лимит в день
+
{taskSummary.dailyUsed}/{taskSummary.dailyLimit}
- )} -
-
Текущая задача
-
-
- - {notifications.length > 0 && ( - {notifications.length} - )} - {notificationsOpen && ( -
-
- Уведомления - +
+
+

Текущая задача

+
{selectedTaskName}
+
+
+
+
+
Группы конкурентов
+
{competitorGroups.length}
+
+
+
Аккаунты
+
{assignedAccountCount}
+
+
+
Очередь
+
{taskStatus.queueCount}
+
+
+
Лимит сегодня
+
{taskStatus.dailyUsed}/{taskStatus.dailyLimit}
+
+
+
+ +
+ +
+
+

Аккаунты

+
Импорт общий
+
+
+
+
+
+

Добавить аккаунт по коду

+ +
+ {manualLoginOpen && ( +
+ {!hasSelectedTask && ( +
Выберите задачу, чтобы добавить аккаунт.
+ )} +
+ +
-
- - - + +
+ +
- {filteredNotifications.length === 0 &&
Пока пусто.
} - {filteredNotifications.map((item) => ( -
- {item.text}{item.count > 1 ? ` (x${item.count})` : ""} -
- ))} +
+ + +
+ {loginStatus &&
{loginStatus}
}
)}
- + +
+

Импорт из tdata

+
+ Можно выбрать сразу несколько папок. Значения по умолчанию — API Telegram Desktop. +
+
+ + +
+ + {tdataLoading &&
Идет импорт, это может занять несколько секунд.
} + {tdataResult && ( +
+
Импортировано: {(tdataResult.imported || []).length}
+
Пропущено: {(tdataResult.skipped || []).length}
+
Ошибок: {(tdataResult.failed || []).length}
+ {(tdataResult.failed || []).length > 0 && ( +
+ {tdataResult.failed.map((item, index) => ( +
+
{item.path}
+
{item.error}
+ {explainTdataError(item.error) && ( +
{explainTdataError(item.error)}
+ )} +
+ ))} +
+ )} +
+ )} +
+
+
+ {notificationsOpen && ( +
setNotificationsOpen(false)}> +
event.stopPropagation()}> +
+

Уведомления

+ +
+
+ + + +
+ {filteredNotifications.length === 0 &&
Пока пусто.
} + {filteredNotifications.map((item) => ( +
+ {item.text}{item.count > 1 ? ` (x${item.count})` : ""} +
+ ))} +
-
+ )} {infoOpen && (
setInfoOpen(false)}>
event.stopPropagation()}> @@ -2098,8 +2339,10 @@ export default function App() { const status = taskStatusMap[task.id]; const statusLabel = status ? (status.running ? "Запущено" : "Остановлено") : "—"; const statusClass = status ? (status.running ? "ok" : "off") : "off"; + const unconfirmedCount = status ? Number(status.unconfirmedCount || 0) : 0; const queueLabel = status ? `Очередь: ${status.queueCount}` : "Очередь: —"; const dailyLabel = status ? `Лимит: ${status.dailyUsed}/${status.dailyLimit}` : "Лимит: —"; + const cycleLabel = status && status.running ? `Цикл: ${formatCountdown(status.nextRunAt)}` : "Цикл: —"; const lastMessageRaw = status && status.monitorInfo && status.monitorInfo.lastMessageAt ? status.monitorInfo.lastMessageAt : ""; @@ -2140,11 +2383,19 @@ export default function App() {
{task.name || `Задача #${task.id}`}
-
{statusLabel}
+
+
{statusLabel}
+ {unconfirmedCount > 0 && ( +
+ Не подтверждено: {unconfirmedCount} +
+ )} +
{queueLabel} {dailyLabel} + {cycleLabel}
@@ -2162,7 +2413,10 @@ export default function App() {

Быстрые действия

-
Задача: {selectedTaskName}
+
+ Задача: {selectedTaskName} + {autosaveNote && {autosaveNote}} +
{taskStatus.running ? "Запущено" : "Остановлено"} @@ -2177,17 +2431,42 @@ export default function App() { {taskStatus.running ? ( ) : ( )}
+
+ + + + +
- -
+
+ -
- - Этот аккаунт должен быть админом в целевой группе и уметь выдавать права другим. - - - -
- -
- Интервалы и лимиты -
- - - - - - - - -
-
-
- Импорт аудитории -
- - - - -
-
- - {fileImportResult && ( -
- Импортировано: {fileImportResult.importedCount} · Пропущено: {fileImportResult.skippedCount} · Ошибок: {fileImportResult.failed.length} -
- )} -
- {fileImportResult && fileImportResult.failed.length > 0 && ( -
- {fileImportResult.failed.map((item, index) => ( -
-
{item.path}
-
{item.error}
-
- ))} +
+
+ Роли ботов и вступление +
+ +
- )} -
-
- Распределение ботов -
- {roleMode === "same" ? ( +
+ + + +
+
+ Режим “один и тот же бот” нужен, когда аккаунт должен быть в конкурентах и в нашей группе для инвайта. +
+
+
+ Инвайт через админов +
+ + + + +
+
+
+ Интервалы и лимиты +
+ + - ) : ( - <> + + + + + +
+
+
+ Импорт аудитории +
+ + + + +
+
+ + {fileImportResult && ( +
+ Импортировано: {fileImportResult.importedCount} · Пропущено: {fileImportResult.skippedCount} · Ошибок: {fileImportResult.failed.length} +
+ )} +
+ {fileImportResult && fileImportResult.failed.length > 0 && ( +
+ {fileImportResult.failed.map((item, index) => ( +
+
{item.path}
+
{item.error}
+
+ ))} +
+ )} +
+
+ Распределение ботов +
+ {roleMode === "same" ? ( - - + ) : ( + <> + + + + )} +
+
+
+ Ошибки аккаунтов + {criticalErrorAccounts.length === 0 && ( +
Ошибок нет.
)} + {criticalErrorAccounts.length > 0 && ( +
+ {criticalErrorAccounts.map((account) => ( +
+
{formatAccountLabel(account)}
+
{account.last_error || "Ошибка сессии"}
+
+ ))} +
+ )} +
+
+ Безопасность +
+ + + + + +
+
+ +
+