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() {
- “Собрать историю” добавляет в очередь пользователей, которые писали ранее. Без этого будут учитываться только новые сообщения. -
-+ “Собрать историю” добавляет в очередь авторов старых сообщений. Без этого учитываются только новые сообщения. +
+ > + )} + {infoTab === "features" && ( +