diff --git a/src/main/index.js b/src/main/index.js index 83415b7..6ce193b 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -802,6 +802,63 @@ const toCsv = (rows, headers) => { return lines.join("\n"); }; +const explainInviteError = (error) => { + if (!error) return ""; + if (error === "USER_ID_INVALID") { + return "Пользователь удален/скрыт; access_hash невалиден для этой сессии; приглашение в канал/чат без валидной сущности."; + } + if (error === "CHAT_WRITE_FORBIDDEN") { + return "Аккаунт не может приглашать: нет прав или он не участник группы."; + } + if (error === "USER_NOT_MUTUAL_CONTACT") { + return "Пользователь не взаимный контакт для добавляющего аккаунта. Обычно это происходит, когда в группе/канале включена опция «добавлять могут только контакты» или у пользователя закрыт приём инвайтов."; + } + if (error === "USER_PRIVACY_RESTRICTED") { + return "Приглашение запрещено пользователем: приватность не позволяет добавлять в группы."; + } + if (error === "USER_NOT_PARTICIPANT") { + return "Аккаунт не состоит в целевой группе или канал приватный."; + } + if (error === "USER_BANNED_IN_CHANNEL") { + return "Пользователь заблокирован в группе или канале назначения."; + } + if (error === "USER_BOT") { + return "Бота нельзя приглашать как обычного пользователя."; + } + if (error === "USER_KICKED") { + return "Пользователь был удален из группы ранее."; + } + if (error === "CHAT_ADMIN_REQUIRED") { + return "Для добавления участников нужны права администратора."; + } + if (error === "USER_ALREADY_PARTICIPANT") { + return "Пользователь уже состоит в целевой группе."; + } + if (error === "CHAT_MEMBER_ADD_FAILED") { + return "Telegram отклонил добавление участника (ограничения приватности, антиспам или запрет инвайтов)."; + } + if (error === "INVITE_HASH_EXPIRED" || error === "INVITE_HASH_INVALID") { + return "Инвайт-ссылка недействительна или истекла."; + } + if (error === "CHANNEL_PRIVATE") { + return "Целевая группа/канал приватные и недоступны по ссылке."; + } + if (error === "AUTH_KEY_DUPLICATED") { + return "Сессия используется в другом месте, Telegram отозвал ключ."; + } + if (error.includes("FLOOD") || error.includes("PEER_FLOOD")) { + return "Ограничение Telegram по частоте действий."; + } + return ""; +}; + +const extractErrorCode = (value) => { + if (!value) return ""; + const text = String(value).trim(); + const split = text.split(/[:(]/, 1); + return split && split[0] ? split[0].trim() : text; +}; + ipcMain.handle("logs:export", async (_event, taskId) => { const { canceled, filePath } = await dialog.showSaveDialog({ title: "Выгрузить логи", @@ -819,9 +876,14 @@ ipcMain.handle("logs:export", async (_event, taskId) => { 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 || []) + errors: JSON.stringify(log.errors || []), + errorsHuman: JSON.stringify((log.errors || []).map((value) => { + const code = extractErrorCode(value); + const explanation = explainInviteError(code); + return explanation ? `${value} (${explanation})` : value; + })) })); - const csv = toCsv(logs, ["taskId", "startedAt", "finishedAt", "invitedCount", "unconfirmedCount", "cycleLimit", "queueCount", "batchSize", "successIds", "errors"]); + const csv = toCsv(logs, ["taskId", "startedAt", "finishedAt", "invitedCount", "unconfirmedCount", "cycleLimit", "queueCount", "batchSize", "successIds", "errors", "errorsHuman"]); fs.writeFileSync(filePath, csv, "utf8"); return { ok: true, filePath }; }); @@ -834,22 +896,37 @@ ipcMain.handle("invites:export", async (_event, taskId) => { if (canceled || !filePath) return { ok: false, canceled: true }; const invites = store.listInvites(2000, taskId); - const csv = toCsv(invites, [ + const enriched = invites.map((invite) => { + const errorCode = extractErrorCode(invite.error); + const skippedCode = extractErrorCode(invite.skippedReason); + const confirmCode = extractErrorCode(invite.confirmError); + return { + ...invite, + errorHuman: explainInviteError(errorCode), + skippedReasonHuman: explainInviteError(skippedCode), + confirmErrorHuman: explainInviteError(confirmCode) + }; + }); + const csv = toCsv(enriched, [ "taskId", "invitedAt", "userId", "username", "status", "error", + "errorHuman", "confirmed", "confirmError", + "confirmErrorHuman", "accountId", "accountPhone", "watcherAccountId", "watcherPhone", "strategy", "strategyMeta", - "sourceChat" + "sourceChat", + "skippedReason", + "skippedReasonHuman" ]); fs.writeFileSync(filePath, csv, "utf8"); return { ok: true, filePath }; @@ -875,9 +952,12 @@ ipcMain.handle("invites:exportProblems", async (_event, taskId) => { username: invite.username ? `@${invite.username}` : "", status: invite.status, error: invite.error || "", + errorHuman: explainInviteError(extractErrorCode(invite.error)), skippedReason: invite.skippedReason || "", + skippedReasonHuman: explainInviteError(extractErrorCode(invite.skippedReason)), confirmed: invite.confirmed, confirmError: invite.confirmError || "", + confirmErrorHuman: explainInviteError(extractErrorCode(invite.confirmError)), invitedAt: invite.invitedAt, sourceChat: invite.sourceChat, targetChat: invite.targetChat @@ -888,9 +968,12 @@ ipcMain.handle("invites:exportProblems", async (_event, taskId) => { "username", "status", "error", + "errorHuman", "skippedReason", + "skippedReasonHuman", "confirmed", "confirmError", + "confirmErrorHuman", "invitedAt", "sourceChat", "targetChat" diff --git a/src/main/store.js b/src/main/store.js index 5aaea1a..ca78704 100644 --- a/src/main/store.js +++ b/src/main/store.js @@ -137,11 +137,11 @@ function initStore(userDataPath) { id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, our_group TEXT NOT NULL, - min_interval_minutes INTEGER NOT NULL, - 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, + min_interval_minutes INTEGER NOT NULL DEFAULT 1, + max_interval_minutes INTEGER NOT NULL DEFAULT 3, + daily_limit INTEGER NOT NULL DEFAULT 15, + history_limit INTEGER NOT NULL DEFAULT 35, + max_invites_per_cycle INTEGER NOT NULL DEFAULT 1, max_competitor_bots INTEGER NOT NULL, max_our_bots INTEGER NOT NULL, random_accounts INTEGER NOT NULL DEFAULT 0, @@ -160,7 +160,7 @@ function initStore(userDataPath) { invite_via_admins INTEGER NOT NULL DEFAULT 0, invite_admin_master_id INTEGER NOT NULL DEFAULT 0, invite_admin_allow_flood INTEGER NOT NULL DEFAULT 0, - warmup_enabled INTEGER NOT NULL DEFAULT 0, + warmup_enabled INTEGER NOT NULL DEFAULT 1, warmup_start_limit INTEGER NOT NULL DEFAULT 3, warmup_daily_increase INTEGER NOT NULL DEFAULT 2, cycle_competitors INTEGER NOT NULL DEFAULT 0, @@ -558,10 +558,18 @@ function initStore(userDataPath) { if (!task.warmup_enabled) return baseLimit; const createdAt = task.created_at ? new Date(task.created_at).getTime() : Date.now(); const days = Math.max(0, Math.floor((Date.now() - createdAt) / (24 * 60 * 60 * 1000))); - const startLimit = Math.max(1, Number(task.warmup_start_limit || 1)); - const step = Math.max(0, Number(task.warmup_daily_increase || 0)); - const warmed = startLimit + days * step; - return Math.min(baseLimit || warmed, warmed); + const dayIndex = days + 1; + let warmed = 7; + if (dayIndex <= 3) warmed = 1; + else if (dayIndex <= 7) warmed = 2; + else if (dayIndex <= 12) warmed = 3; + else if (dayIndex <= 18) warmed = 4; + else if (dayIndex <= 25) warmed = 5; + else if (dayIndex <= 33) warmed = 6; + if (baseLimit > 0) { + return Math.min(baseLimit, warmed); + } + return warmed; } function saveTask(task) { @@ -584,7 +592,7 @@ function initStore(userDataPath) { task.maxIntervalMinutes, task.dailyLimit, task.historyLimit, - task.maxInvitesPerCycle || 20, + task.maxInvitesPerCycle || 1, task.maxCompetitorBots, task.maxOurBots, task.randomAccounts ? 1 : 0, @@ -630,7 +638,7 @@ function initStore(userDataPath) { task.maxIntervalMinutes, task.dailyLimit, task.historyLimit, - task.maxInvitesPerCycle || 20, + task.maxInvitesPerCycle || 1, task.maxCompetitorBots, task.maxOurBots, task.randomAccounts ? 1 : 0, diff --git a/src/renderer/App.jsx b/src/renderer/App.jsx index 2e69dc4..108c647 100644 --- a/src/renderer/App.jsx +++ b/src/renderer/App.jsx @@ -22,11 +22,11 @@ const emptySettings = { id: null, name: "", ourGroup: "", - minIntervalMinutes: 5, - maxIntervalMinutes: 10, - dailyLimit: 100, - historyLimit: 100, - maxInvitesPerCycle: 20, + minIntervalMinutes: 1, + maxIntervalMinutes: 3, + dailyLimit: 15, + historyLimit: 35, + maxInvitesPerCycle: 1, maxCompetitorBots: 1, maxOurBots: 1, randomAccounts: false, @@ -40,7 +40,7 @@ const emptySettings = { inviteViaAdmins: false, inviteAdminMasterId: 0, inviteAdminAllowFlood: false, - warmupEnabled: false, + warmupEnabled: true, warmupStartLimit: 3, warmupDailyIncrease: 2, cycleCompetitors: false, @@ -58,11 +58,11 @@ const emptySettings = { id: row.id, name: row.name || "", ourGroup: row.our_group || "", - minIntervalMinutes: Number(row.min_interval_minutes || 5), - 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), + minIntervalMinutes: Number(row.min_interval_minutes || 1), + maxIntervalMinutes: Number(row.max_interval_minutes || 3), + dailyLimit: Number(row.daily_limit || 15), + historyLimit: Number(row.history_limit || 35), + maxInvitesPerCycle: Number(row.max_invites_per_cycle || 1), maxCompetitorBots: Number(row.max_competitor_bots || 1), maxOurBots: Number(row.max_our_bots || 1), randomAccounts: Boolean(row.random_accounts), @@ -76,7 +76,7 @@ const emptySettings = { inviteViaAdmins: Boolean(row.invite_via_admins), inviteAdminMasterId: Number(row.invite_admin_master_id || 0), inviteAdminAllowFlood: Boolean(row.invite_admin_allow_flood), - warmupEnabled: Boolean(row.warmup_enabled), + warmupEnabled: row.warmup_enabled == null ? true : Boolean(row.warmup_enabled), warmupStartLimit: Number(row.warmup_start_limit || 3), warmupDailyIncrease: Number(row.warmup_daily_increase || 2), cycleCompetitors: Boolean(row.cycle_competitors), @@ -723,6 +723,9 @@ export default function App() { if (error === "USER_ALREADY_PARTICIPANT") { return "Пользователь уже состоит в целевой группе."; } + if (error === "CHAT_MEMBER_ADD_FAILED") { + return "Telegram отклонил добавление участника (ограничения приватности, антиспам или запрет инвайтов)."; + } if (error === "INVITE_HASH_EXPIRED" || error === "INVITE_HASH_INVALID") { return "Инвайт-ссылка недействительна или истекла."; } @@ -2764,42 +2767,51 @@ export default function App() { onChange={(event) => setTaskForm({ ...taskForm, warmupEnabled: event.target.checked })} /> Разогрев лимита - Плавно увеличивает дневной лимит по дням. - - - + {!taskForm.warmupEnabled && ( + <> + + + + )}
diff --git a/src/renderer/styles/app.css b/src/renderer/styles/app.css index 7863331..b96afe9 100644 --- a/src/renderer/styles/app.css +++ b/src/renderer/styles/app.css @@ -516,7 +516,7 @@ body { inset: 0; background: rgba(15, 23, 42, 0.4); display: flex; - align-items: center; + align-items: flex-start; justify-content: center; z-index: 20; padding: 20px; @@ -527,6 +527,8 @@ body { border-radius: 16px; padding: 24px; width: min(640px, 100%); + max-height: 80vh; + overflow: auto; display: flex; flex-direction: column; gap: 16px; @@ -607,7 +609,7 @@ body { .task-columns { display: grid; - grid-template-columns: minmax(0, 1.2fr) minmax(0, 0.8fr); + grid-template-columns: minmax(0, 1fr); gap: 20px; width: 100%; } diff --git a/src/renderer/tabs/LogsTab.jsx b/src/renderer/tabs/LogsTab.jsx index e12fda1..387f5b8 100644 --- a/src/renderer/tabs/LogsTab.jsx +++ b/src/renderer/tabs/LogsTab.jsx @@ -101,6 +101,19 @@ function LogsTab({ if (value === "group") return "группа"; return value; }; + const formatErrorWithExplain = (value) => { + if (!value) return "—"; + const code = String(value); + const explanation = explainInviteError(code.split(/[:(]/, 1)[0].trim()); + if (!explanation) return code; + if (code.includes("(")) return code; + return `${code} (${explanation})`; + }; + const explainRawError = (value) => { + if (!value) return ""; + const code = String(value).split(/[:(]/, 1)[0].trim(); + return explainInviteError(code); + }; const getDurationMs = (start, finish) => { const startMs = new Date(start).getTime(); @@ -434,19 +447,14 @@ function LogsTab({ return account ? formatAccountLabel(account) : (invite.watcherPhone || "—"); })()} {invite.skippedReason && invite.skippedReason !== "" && ( -
Пропуск: {invite.skippedReason}
+
Результат: {formatErrorWithExplain(invite.skippedReason)}
)} {invite.error && invite.error !== "" && ( -
Причина: {invite.error}
- )} - {invite.error && ( -
- Вероятная причина: {explainInviteError(invite.error) || "Причина не определена"} -
+
Ошибка: {formatErrorWithExplain(invite.error)}
)} {invite.confirmError && (
- Подтверждение: {invite.confirmError} + Проверка участия: {formatErrorWithExplain(invite.confirmError)}
)} {invite.strategy && ( @@ -478,14 +486,14 @@ function LogsTab({
Тип цели: {formatTargetType(invite.targetType)}
Действие: {invite.action || "invite"}
Статус: {formatInviteStatus(invite.status)}
-
Пропуск: {invite.skippedReason || "—"}
-
Ошибка: {invite.error || "—"}
-
Подтверждение: {invite.confirmError || "—"}
+
Результат: {formatErrorWithExplain(invite.skippedReason)}
+
Ошибка: {formatErrorWithExplain(invite.error)}
+
Проверка участия: {formatErrorWithExplain(invite.confirmError)}
{invite.watcherAccountId && invite.accountId && invite.watcherAccountId !== invite.accountId && selectedTask && selectedTask.randomAccounts && hasBothRoles(invite.watcherAccountId) && (
Примечание: у наблюдателя стоят обе роли, но включен случайный выбор — инвайт выполнен другим аккаунтом.
)} -
Вероятная причина: {explainInviteError(invite.error) || "Причина не определена"}
+
Пояснение: {explainRawError(invite.error) || explainRawError(invite.confirmError) || "Причина не определена"}
Стратегия: {invite.strategy || "—"}
{invite.strategyMeta ? `Стратегии:\n${formatStrategies(invite.strategyMeta)}` : "Стратегии: —"}