some
This commit is contained in:
parent
a5d55012a8
commit
d299eb36be
@ -13,6 +13,97 @@ let telegram;
|
||||
let scheduler;
|
||||
const taskRunners = new Map();
|
||||
|
||||
const filterTaskRolesByAccounts = (taskId, roles, accounts) => {
|
||||
const accountMap = new Map(accounts.map((acc) => [acc.id, acc]));
|
||||
const filtered = [];
|
||||
let removedMissing = 0;
|
||||
let removedError = 0;
|
||||
roles.forEach((row) => {
|
||||
const account = accountMap.get(row.account_id);
|
||||
if (!account) {
|
||||
removedMissing += 1;
|
||||
return;
|
||||
}
|
||||
if (account.status && account.status !== "ok") {
|
||||
removedError += 1;
|
||||
return;
|
||||
}
|
||||
filtered.push({
|
||||
accountId: row.account_id,
|
||||
roleMonitor: Boolean(row.role_monitor),
|
||||
roleInvite: Boolean(row.role_invite)
|
||||
});
|
||||
});
|
||||
if (removedMissing || removedError) {
|
||||
store.setTaskAccountRoles(taskId, filtered);
|
||||
}
|
||||
return { filtered, removedMissing, removedError };
|
||||
};
|
||||
|
||||
const startTaskWithChecks = async (id) => {
|
||||
const task = store.getTask(id);
|
||||
if (!task) return { ok: false, error: "Task not found" };
|
||||
const competitors = store.listTaskCompetitors(id).map((row) => row.link);
|
||||
const taskAccounts = store.listTaskAccounts(id);
|
||||
const existingAccounts = store.listAccounts();
|
||||
const filteredResult = filterTaskRolesByAccounts(id, taskAccounts, existingAccounts);
|
||||
const filteredRoles = filteredResult.filtered;
|
||||
const inviteIds = filteredRoles.filter((row) => row.roleInvite).map((row) => row.accountId);
|
||||
const monitorIds = filteredRoles.filter((row) => row.roleMonitor).map((row) => row.accountId);
|
||||
if (!inviteIds.length) {
|
||||
return { ok: false, error: "Нет аккаунтов с ролью инвайта." };
|
||||
}
|
||||
if (!monitorIds.length) {
|
||||
return { ok: false, error: "Нет аккаунтов с ролью мониторинга." };
|
||||
}
|
||||
const accessCheck = await telegram.checkGroupAccess(competitors, task.our_group);
|
||||
if (accessCheck && accessCheck.ok) {
|
||||
const ourAccess = accessCheck.result.find((item) => item.type === "our");
|
||||
if (ourAccess && !ourAccess.ok) {
|
||||
return { ok: false, error: `Нет доступа к нашей группе: ${ourAccess.details || ourAccess.value}` };
|
||||
}
|
||||
}
|
||||
const inviteAccess = await telegram.checkInvitePermissions(task, inviteIds);
|
||||
if (inviteAccess && inviteAccess.ok) {
|
||||
store.setTaskInviteAccess(id, inviteAccess.result || []);
|
||||
} else if (inviteAccess && inviteAccess.error) {
|
||||
return { ok: false, error: inviteAccess.error };
|
||||
}
|
||||
|
||||
let runner = taskRunners.get(id);
|
||||
if (!runner) {
|
||||
runner = new TaskRunner(store, telegram, task);
|
||||
taskRunners.set(id, runner);
|
||||
} else {
|
||||
runner.task = task;
|
||||
}
|
||||
await runner.start();
|
||||
const warnings = [];
|
||||
if (accessCheck && accessCheck.ok) {
|
||||
const competitorIssues = accessCheck.result.filter((item) => item.type === "competitor" && !item.ok);
|
||||
if (competitorIssues.length) {
|
||||
warnings.push(`Нет доступа к ${competitorIssues.length} группе(ам) конкурентов.`);
|
||||
}
|
||||
}
|
||||
if (inviteAccess && inviteAccess.ok) {
|
||||
const missingSessions = (inviteAccess.result || []).filter((row) => !row.ok || row.reason === "Сессия не подключена");
|
||||
if (missingSessions.length) {
|
||||
warnings.push(`Сессии не подключены: ${missingSessions.length} аккаунт(ов).`);
|
||||
}
|
||||
const noRights = (inviteAccess.result || []).filter((row) => !row.canInvite);
|
||||
if (noRights.length) {
|
||||
warnings.push(`Нет прав инвайта у ${noRights.length} аккаунт(ов).`);
|
||||
}
|
||||
}
|
||||
if (filteredResult.removedError) {
|
||||
warnings.push(`Отключены аккаунты с ошибкой: ${filteredResult.removedError}.`);
|
||||
}
|
||||
if (filteredResult.removedMissing) {
|
||||
warnings.push(`Удалены отсутствующие аккаунты: ${filteredResult.removedMissing}.`);
|
||||
}
|
||||
return { ok: true, warnings };
|
||||
};
|
||||
|
||||
function createWindow() {
|
||||
const iconPath = path.join(__dirname, "..", "..", "resources", "icon.png");
|
||||
mainWindow = new BrowserWindow({
|
||||
@ -236,17 +327,7 @@ ipcMain.handle("tasks:delete", (_event, id) => {
|
||||
return { ok: true };
|
||||
});
|
||||
ipcMain.handle("tasks:start", async (_event, id) => {
|
||||
const task = store.getTask(id);
|
||||
if (!task) return { ok: false, error: "Task not found" };
|
||||
let runner = taskRunners.get(id);
|
||||
if (!runner) {
|
||||
runner = new TaskRunner(store, telegram, task);
|
||||
taskRunners.set(id, runner);
|
||||
} else {
|
||||
runner.task = task;
|
||||
}
|
||||
await runner.start();
|
||||
return { ok: true };
|
||||
return startTaskWithChecks(id);
|
||||
});
|
||||
ipcMain.handle("tasks:stop", (_event, id) => {
|
||||
const runner = taskRunners.get(id);
|
||||
@ -310,15 +391,12 @@ ipcMain.handle("tasks:startAll", async () => {
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
let runner = taskRunners.get(task.id);
|
||||
if (!runner) {
|
||||
runner = new TaskRunner(store, telegram, task);
|
||||
taskRunners.set(task.id, runner);
|
||||
} else {
|
||||
runner.task = task;
|
||||
}
|
||||
try {
|
||||
await runner.start();
|
||||
const result = await startTaskWithChecks(task.id);
|
||||
if (!result.ok) {
|
||||
errors.push({ id: task.id, error: result.error || "start failed" });
|
||||
continue;
|
||||
}
|
||||
started += 1;
|
||||
} catch (error) {
|
||||
errors.push({ id: task.id, error: error.message || String(error) });
|
||||
@ -342,24 +420,48 @@ ipcMain.handle("tasks:status", (_event, id) => {
|
||||
const task = store.getTask(id);
|
||||
const monitorInfo = telegram.getTaskMonitorInfo(id);
|
||||
const warnings = [];
|
||||
const readiness = { ok: true, reasons: [] };
|
||||
if (task) {
|
||||
const accountRows = store.listTaskAccounts(id);
|
||||
const accounts = store.listAccounts();
|
||||
const accountsById = new Map(accounts.map((acc) => [acc.id, acc]));
|
||||
if (runner && runner.isRunning()) {
|
||||
const sanitized = filterTaskRolesByAccounts(id, accountRows, accounts);
|
||||
if (sanitized.removedError || sanitized.removedMissing) {
|
||||
warnings.push(`Авто-синхронизация ролей: удалено ${sanitized.removedMissing + sanitized.removedError} аккаунт(ов).`);
|
||||
}
|
||||
if (!sanitized.filtered.length) {
|
||||
warnings.push("Задача остановлена: нет доступных аккаунтов.");
|
||||
runner.stop();
|
||||
}
|
||||
}
|
||||
const inviteRows = accountRows.filter((row) => row.role_invite);
|
||||
const monitorRows = accountRows.filter((row) => row.role_monitor);
|
||||
if (!inviteRows.length) {
|
||||
readiness.ok = false;
|
||||
readiness.reasons.push("Нет аккаунтов с ролью инвайта.");
|
||||
}
|
||||
if (!monitorRows.length) {
|
||||
readiness.ok = false;
|
||||
readiness.reasons.push("Нет аккаунтов с ролью мониторинга.");
|
||||
}
|
||||
if (task.require_same_bot_in_both) {
|
||||
const hasSame = accountRows.some((row) => row.role_monitor && row.role_invite);
|
||||
if (!hasSame) {
|
||||
warnings.push("Режим “один бот в обоих группах” включен, но нет аккаунта с обеими ролями.");
|
||||
readiness.ok = false;
|
||||
readiness.reasons.push("Нет аккаунта с двумя ролями в режиме “один бот”.");
|
||||
}
|
||||
}
|
||||
const allAssignments = store.listAllTaskAccounts();
|
||||
const accountMap = new Map();
|
||||
const accountTaskMap = new Map();
|
||||
allAssignments.forEach((row) => {
|
||||
if (!accountMap.has(row.account_id)) accountMap.set(row.account_id, new Set());
|
||||
accountMap.get(row.account_id).add(row.task_id);
|
||||
if (!accountTaskMap.has(row.account_id)) accountTaskMap.set(row.account_id, new Set());
|
||||
accountTaskMap.get(row.account_id).add(row.task_id);
|
||||
});
|
||||
const accountsById = new Map(store.listAccounts().map((acc) => [acc.id, acc]));
|
||||
const seen = new Set();
|
||||
accountRows.forEach((row) => {
|
||||
const tasksForAccount = accountMap.get(row.account_id);
|
||||
const tasksForAccount = accountTaskMap.get(row.account_id);
|
||||
if (tasksForAccount && tasksForAccount.size > 1 && !seen.has(row.account_id)) {
|
||||
seen.add(row.account_id);
|
||||
const account = accountsById.get(row.account_id);
|
||||
@ -367,6 +469,57 @@ ipcMain.handle("tasks:status", (_event, id) => {
|
||||
warnings.push(`Аккаунт ${label} используется в ${tasksForAccount.size} задачах. Лимиты действий/групп общие.`);
|
||||
}
|
||||
});
|
||||
|
||||
if (inviteRows.length) {
|
||||
const inviteAccounts = inviteRows.map((row) => accountsById.get(row.account_id)).filter(Boolean);
|
||||
const badSessions = inviteAccounts.filter((acc) => acc.status && acc.status !== "ok");
|
||||
if (badSessions.length) {
|
||||
readiness.ok = false;
|
||||
readiness.reasons.push(`Есть аккаунты с ошибкой сессии: ${badSessions.length}.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (runner && runner.isRunning() && queueCount === 0) {
|
||||
if (!monitorInfo || !monitorInfo.monitoring) {
|
||||
warnings.push("Очередь пуста: мониторинг не активен.");
|
||||
} else if (!monitorInfo.groups || monitorInfo.groups.length === 0) {
|
||||
warnings.push("Очередь пуста: нет групп в мониторинге.");
|
||||
} else if (!monitorInfo.lastMessageAt) {
|
||||
warnings.push("Очередь пуста: новых сообщений пока нет.");
|
||||
} else {
|
||||
warnings.push(`Очередь пуста: последнее сообщение ${monitorInfo.lastMessageAt}.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (task.our_group && (task.our_group.includes("joinchat/") || task.our_group.includes("t.me/+"))) {
|
||||
warnings.push("Целевая группа указана по инвайт-ссылке — доступ может быть ограничен.");
|
||||
}
|
||||
|
||||
if (task.task_invite_access) {
|
||||
try {
|
||||
const parsed = JSON.parse(task.task_invite_access);
|
||||
if (Array.isArray(parsed) && parsed.length) {
|
||||
const total = parsed.length;
|
||||
const canInvite = parsed.filter((row) => row.canInvite).length;
|
||||
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 ? ` Проверка: ${checkedAt}.` : ""}`);
|
||||
if (disconnected) {
|
||||
warnings.push(`Сессия не подключена: ${disconnected} аккаунт(ов).`);
|
||||
}
|
||||
if (isChannel) {
|
||||
warnings.push("Цель — канал: добавлять участников могут только админы.");
|
||||
}
|
||||
if (canInvite === 0) {
|
||||
readiness.ok = false;
|
||||
readiness.reasons.push("Нет аккаунтов с правами инвайта.");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore parsing errors
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
running: runner ? runner.isRunning() : false,
|
||||
@ -379,7 +532,8 @@ ipcMain.handle("tasks:status", (_event, id) => {
|
||||
nextInviteAccountId: runner ? runner.getNextInviteAccountId() : 0,
|
||||
lastInviteAccountId: runner ? runner.getLastInviteAccountId() : 0,
|
||||
pendingStats: store.getPendingStats(id),
|
||||
warnings
|
||||
warnings,
|
||||
readiness
|
||||
};
|
||||
});
|
||||
|
||||
@ -425,7 +579,11 @@ ipcMain.handle("tasks:checkInviteAccess", async (_event, id) => {
|
||||
const accountIds = accountRows
|
||||
.filter((row) => existingIds.has(row.account_id))
|
||||
.map((row) => row.account_id);
|
||||
return telegram.checkInvitePermissions(task, accountIds);
|
||||
const result = await telegram.checkInvitePermissions(task, accountIds);
|
||||
if (result && result.ok) {
|
||||
store.setTaskInviteAccess(id, result.result || []);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
ipcMain.handle("tasks:groupVisibility", async (_event, id) => {
|
||||
@ -538,6 +696,7 @@ ipcMain.handle("status:get", () => {
|
||||
const dailyRemaining = Math.max(0, Number(settings.dailyLimit || 0) - dailyUsed);
|
||||
const queueCount = store.getPendingCount();
|
||||
const accounts = store.listAccounts();
|
||||
const connectedSessions = telegram.getConnectedCount();
|
||||
const accountStats = accounts.map((account) => {
|
||||
const used = store.countInvitesTodayByAccount(account.id);
|
||||
const limit = Number(account.daily_limit || settings.accountDailyLimit || 0);
|
||||
@ -556,6 +715,8 @@ ipcMain.handle("status:get", () => {
|
||||
dailyRemaining,
|
||||
dailyUsed,
|
||||
dailyLimit: Number(settings.dailyLimit || 0),
|
||||
connectedSessions,
|
||||
totalAccounts: accounts.length,
|
||||
accountStats,
|
||||
monitorInfo
|
||||
};
|
||||
|
||||
@ -13,6 +13,7 @@ const DEFAULT_SETTINGS = {
|
||||
accountMaxGroups: 10,
|
||||
accountDailyLimit: 50,
|
||||
floodCooldownMinutes: 1440,
|
||||
queueTtlHours: 24,
|
||||
autoJoinCompetitors: false,
|
||||
autoJoinOurGroup: false
|
||||
};
|
||||
@ -125,6 +126,8 @@ function initStore(userDataPath) {
|
||||
stop_blocked_percent INTEGER NOT NULL DEFAULT 25,
|
||||
notes TEXT NOT NULL DEFAULT '',
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
task_invite_access TEXT NOT NULL DEFAULT '',
|
||||
task_invite_access_at TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
@ -183,6 +186,8 @@ function initStore(userDataPath) {
|
||||
ensureColumn("tasks", "auto_join_our_group", "INTEGER NOT NULL DEFAULT 1");
|
||||
ensureColumn("tasks", "separate_bot_roles", "INTEGER NOT NULL DEFAULT 0");
|
||||
ensureColumn("tasks", "require_same_bot_in_both", "INTEGER NOT NULL DEFAULT 0");
|
||||
ensureColumn("tasks", "task_invite_access", "TEXT NOT NULL DEFAULT ''");
|
||||
ensureColumn("tasks", "task_invite_access_at", "TEXT NOT NULL DEFAULT ''");
|
||||
ensureColumn("task_accounts", "role_monitor", "INTEGER NOT NULL DEFAULT 1");
|
||||
ensureColumn("task_accounts", "role_invite", "INTEGER NOT NULL DEFAULT 1");
|
||||
|
||||
@ -399,6 +404,18 @@ function initStore(userDataPath) {
|
||||
db.prepare("DELETE FROM invite_queue WHERE task_id = ?").run(taskId || 0);
|
||||
}
|
||||
|
||||
function clearQueueOlderThan(taskId, hours) {
|
||||
const limit = Number(hours || 0);
|
||||
if (!Number.isFinite(limit) || limit <= 0) return 0;
|
||||
const cutoff = dayjs().subtract(limit, "hour").toISOString();
|
||||
if (taskId == null) {
|
||||
const result = db.prepare("DELETE FROM invite_queue WHERE created_at < ?").run(cutoff);
|
||||
return result.changes || 0;
|
||||
}
|
||||
const result = db.prepare("DELETE FROM invite_queue WHERE task_id = ? AND created_at < ?").run(taskId || 0, cutoff);
|
||||
return result.changes || 0;
|
||||
}
|
||||
|
||||
function listTasks() {
|
||||
return db.prepare("SELECT * FROM tasks ORDER BY id DESC").all();
|
||||
}
|
||||
@ -474,6 +491,13 @@ function initStore(userDataPath) {
|
||||
return result.lastInsertRowid;
|
||||
}
|
||||
|
||||
function setTaskInviteAccess(taskId, payload) {
|
||||
const now = dayjs().toISOString();
|
||||
const value = payload ? JSON.stringify(payload) : "";
|
||||
db.prepare("UPDATE tasks SET task_invite_access = ?, task_invite_access_at = ?, updated_at = ? WHERE id = ?")
|
||||
.run(value, value ? now : "", now, taskId);
|
||||
}
|
||||
|
||||
function deleteTask(id) {
|
||||
db.prepare("DELETE FROM tasks WHERE id = ?").run(id);
|
||||
db.prepare("DELETE FROM task_competitors WHERE task_id = ?").run(id);
|
||||
@ -721,6 +745,7 @@ function initStore(userDataPath) {
|
||||
listTasks,
|
||||
getTask,
|
||||
saveTask,
|
||||
setTaskInviteAccess,
|
||||
deleteTask,
|
||||
listTaskCompetitors,
|
||||
setTaskCompetitors,
|
||||
@ -746,6 +771,7 @@ function initStore(userDataPath) {
|
||||
getPendingCount,
|
||||
getPendingStats,
|
||||
clearQueue,
|
||||
clearQueueOlderThan,
|
||||
markInviteStatus,
|
||||
incrementInviteAttempt,
|
||||
recordInvite,
|
||||
|
||||
@ -75,6 +75,11 @@ class TaskRunner {
|
||||
this.nextInviteAccountId = 0;
|
||||
|
||||
try {
|
||||
const settings = this.store.getSettings();
|
||||
const ttlHours = Number(settings.queueTtlHours || 0);
|
||||
if (ttlHours > 0) {
|
||||
this.store.clearQueueOlderThan(this.task.id, ttlHours);
|
||||
}
|
||||
const accounts = this.store.listTaskAccounts(this.task.id).map((row) => row.account_id);
|
||||
let inviteAccounts = accounts;
|
||||
const roles = this.telegram.getTaskRoleAssignments(this.task.id);
|
||||
|
||||
@ -2085,6 +2085,10 @@ class TelegramManager {
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
getConnectedCount() {
|
||||
return this.clients.size;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { TelegramManager };
|
||||
|
||||
@ -14,7 +14,8 @@ const emptySettings = {
|
||||
historyLimit: 200,
|
||||
accountMaxGroups: 10,
|
||||
accountDailyLimit: 50,
|
||||
floodCooldownMinutes: 1440
|
||||
floodCooldownMinutes: 1440,
|
||||
queueTtlHours: 24
|
||||
};
|
||||
|
||||
const emptyTaskForm = {
|
||||
@ -89,6 +90,7 @@ export default function App() {
|
||||
const [accounts, setAccounts] = useState([]);
|
||||
const [accountStats, setAccountStats] = useState([]);
|
||||
const [accountAssignments, setAccountAssignments] = useState([]);
|
||||
const [globalStatus, setGlobalStatus] = useState({ connectedSessions: 0, totalAccounts: 0 });
|
||||
const [logs, setLogs] = useState([]);
|
||||
const [invites, setInvites] = useState([]);
|
||||
const [tasks, setTasks] = useState([]);
|
||||
@ -129,6 +131,7 @@ export default function App() {
|
||||
});
|
||||
const [tdataResult, setTdataResult] = useState(null);
|
||||
const [tdataLoading, setTdataLoading] = useState(false);
|
||||
const [taskActionLoading, setTaskActionLoading] = useState(false);
|
||||
const [loginId, setLoginId] = useState("");
|
||||
const [loginStatus, setLoginStatus] = useState("");
|
||||
const [taskNotice, setTaskNotice] = useState(null);
|
||||
@ -203,6 +206,17 @@ export default function App() {
|
||||
});
|
||||
return { monitor, invite };
|
||||
}, [taskAccountRoles]);
|
||||
const roleIntersectionCount = useMemo(() => {
|
||||
let count = 0;
|
||||
Object.values(taskAccountRoles).forEach((roles) => {
|
||||
if (roles.monitor && roles.invite) count += 1;
|
||||
});
|
||||
return count;
|
||||
}, [taskAccountRoles]);
|
||||
const assignedAccountCount = useMemo(() => {
|
||||
const ids = new Set([...roleSummary.monitor, ...roleSummary.invite]);
|
||||
return ids.size;
|
||||
}, [roleSummary]);
|
||||
const assignedAccountMap = useMemo(() => {
|
||||
const map = new Map();
|
||||
accountAssignments.forEach((row) => {
|
||||
@ -334,6 +348,10 @@ export default function App() {
|
||||
setAccounts(accountsData);
|
||||
setAccountEvents(eventsData);
|
||||
setAccountStats(statusData.accountStats || []);
|
||||
setGlobalStatus({
|
||||
connectedSessions: statusData.connectedSessions || 0,
|
||||
totalAccounts: statusData.totalAccounts || 0
|
||||
});
|
||||
const tasksData = await loadTasks();
|
||||
await loadAccountAssignments();
|
||||
await loadTaskStatuses(tasksData);
|
||||
@ -349,6 +367,10 @@ export default function App() {
|
||||
setInviteAccessStatus([]);
|
||||
setMembershipStatus({});
|
||||
setTaskNotice(null);
|
||||
if (selectedTaskId != null) {
|
||||
checkAccess("auto", true);
|
||||
checkInviteAccess("auto", true);
|
||||
}
|
||||
}, [selectedTaskId]);
|
||||
|
||||
const taskSummary = useMemo(() => {
|
||||
@ -447,6 +469,10 @@ export default function App() {
|
||||
setAccountAssignments(await window.api.listAccountAssignments());
|
||||
const statusData = await window.api.getStatus();
|
||||
setAccountStats(statusData.accountStats || []);
|
||||
setGlobalStatus({
|
||||
connectedSessions: statusData.connectedSessions || 0,
|
||||
totalAccounts: statusData.totalAccounts || 0
|
||||
});
|
||||
} finally {
|
||||
accountsPollInFlight.current = false;
|
||||
}
|
||||
@ -516,6 +542,39 @@ export default function App() {
|
||||
}));
|
||||
}, [selectedTaskId, taskStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasSelectedTask) return;
|
||||
if (taskForm.requireSameBotInBoth) {
|
||||
const nextValue = Math.max(1, roleIntersectionCount || 0);
|
||||
if (taskForm.maxCompetitorBots !== nextValue || taskForm.maxOurBots !== nextValue) {
|
||||
setTaskForm((prev) => sanitizeTaskForm({
|
||||
...prev,
|
||||
maxCompetitorBots: nextValue,
|
||||
maxOurBots: nextValue
|
||||
}));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (taskForm.separateBotRoles) {
|
||||
const nextCompetitors = Math.max(1, roleSummary.monitor.length || 0);
|
||||
const nextOur = Math.max(1, roleSummary.invite.length || 0);
|
||||
if (taskForm.maxCompetitorBots !== nextCompetitors || taskForm.maxOurBots !== nextOur) {
|
||||
setTaskForm((prev) => ({
|
||||
...prev,
|
||||
maxCompetitorBots: nextCompetitors,
|
||||
maxOurBots: nextOur
|
||||
}));
|
||||
}
|
||||
}
|
||||
}, [
|
||||
hasSelectedTask,
|
||||
roleIntersectionCount,
|
||||
roleSummary.monitor.length,
|
||||
roleSummary.invite.length,
|
||||
taskForm.requireSameBotInBoth,
|
||||
taskForm.separateBotRoles
|
||||
]);
|
||||
|
||||
const formatAccountStatus = (status) => {
|
||||
if (status === "limited") return "В спаме";
|
||||
if (status === "error") return "Ошибка";
|
||||
@ -830,6 +889,23 @@ export default function App() {
|
||||
showNotification("Сохраняем задачу...", "info");
|
||||
const nextForm = sanitizeTaskForm(taskForm);
|
||||
setTaskForm(nextForm);
|
||||
const validateLink = (value) => {
|
||||
const trimmed = String(value || "").trim();
|
||||
if (!trimmed) return false;
|
||||
if (trimmed.startsWith("@")) return true;
|
||||
if (trimmed.startsWith("https://t.me/")) return true;
|
||||
if (trimmed.startsWith("http://t.me/")) return true;
|
||||
return false;
|
||||
};
|
||||
const invalidCompetitors = competitorGroups.filter((link) => !validateLink(link));
|
||||
if (!validateLink(nextForm.ourGroup)) {
|
||||
showNotification("Наша группа должна быть ссылкой t.me или @username.", "error");
|
||||
return;
|
||||
}
|
||||
if (invalidCompetitors.length) {
|
||||
showNotification(`Некорректные ссылки конкурентов: ${invalidCompetitors.join(", ")}`, "error");
|
||||
return;
|
||||
}
|
||||
let accountRolesMap = { ...taskAccountRoles };
|
||||
let accountIds = Object.keys(accountRolesMap).map((id) => Number(id));
|
||||
if (nextForm.requireSameBotInBoth) {
|
||||
@ -931,11 +1007,16 @@ export default function App() {
|
||||
showNotification("Сначала выберите задачу.", "error");
|
||||
return;
|
||||
}
|
||||
if (taskActionLoading) return;
|
||||
setTaskActionLoading(true);
|
||||
showNotification("Запуск...", "info");
|
||||
try {
|
||||
const result = await window.api.startTaskById(selectedTaskId);
|
||||
if (result && result.ok) {
|
||||
setTaskNotice({ text: "Запущено.", tone: "success", source });
|
||||
if (result.warnings && result.warnings.length) {
|
||||
showNotification(`Предупреждения: ${result.warnings.join(" | ")}`, "info");
|
||||
}
|
||||
} else {
|
||||
showNotification(result.error || "Не удалось запустить", "error");
|
||||
}
|
||||
@ -943,6 +1024,8 @@ export default function App() {
|
||||
const message = error.message || String(error);
|
||||
setTaskNotice({ text: message, tone: "error", source });
|
||||
showNotification(message, "error");
|
||||
} finally {
|
||||
setTaskActionLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@ -974,9 +1057,11 @@ export default function App() {
|
||||
showNotification("Сначала выберите задачу.", "error");
|
||||
return;
|
||||
}
|
||||
if (taskActionLoading) return;
|
||||
if (!window.confirm(`Остановить задачу: ${selectedTaskName}?`)) {
|
||||
return;
|
||||
}
|
||||
setTaskActionLoading(true);
|
||||
showNotification("Остановка...", "info");
|
||||
try {
|
||||
await window.api.stopTaskById(selectedTaskId);
|
||||
@ -985,6 +1070,8 @@ export default function App() {
|
||||
const message = error.message || String(error);
|
||||
setTaskNotice({ text: message, tone: "error", source });
|
||||
showNotification(message, "error");
|
||||
} finally {
|
||||
setTaskActionLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@ -1060,42 +1147,42 @@ export default function App() {
|
||||
}
|
||||
};
|
||||
|
||||
const checkAccess = async (source = "editor") => {
|
||||
const checkAccess = async (source = "editor", silent = false) => {
|
||||
if (!window.api || selectedTaskId == null) {
|
||||
showNotification("Сначала выберите задачу.", "error");
|
||||
if (!silent) showNotification("Сначала выберите задачу.", "error");
|
||||
return;
|
||||
}
|
||||
showNotification("Проверяем доступ к группам...", "info");
|
||||
if (!silent) showNotification("Проверяем доступ к группам...", "info");
|
||||
try {
|
||||
const result = await window.api.checkAccessByTask(selectedTaskId);
|
||||
if (!result.ok) {
|
||||
showNotification(result.error || "Не удалось проверить доступ", "error");
|
||||
if (!silent) showNotification(result.error || "Не удалось проверить доступ", "error");
|
||||
return;
|
||||
}
|
||||
setAccessStatus(result.result || []);
|
||||
setTaskNotice({ text: "Проверка доступа завершена.", tone: "success", source });
|
||||
if (!silent) setTaskNotice({ text: "Проверка доступа завершена.", tone: "success", source });
|
||||
} catch (error) {
|
||||
showNotification(error.message || String(error), "error");
|
||||
if (!silent) showNotification(error.message || String(error), "error");
|
||||
}
|
||||
};
|
||||
|
||||
const checkInviteAccess = async (source = "editor") => {
|
||||
const checkInviteAccess = async (source = "editor", silent = false) => {
|
||||
if (!window.api || selectedTaskId == null) {
|
||||
showNotification("Сначала выберите задачу.", "error");
|
||||
if (!silent) showNotification("Сначала выберите задачу.", "error");
|
||||
return;
|
||||
}
|
||||
setInviteAccessStatus([]);
|
||||
showNotification("Проверяем права инвайта...", "info");
|
||||
if (!silent) showNotification("Проверяем права инвайта...", "info");
|
||||
try {
|
||||
const result = await window.api.checkInviteAccessByTask(selectedTaskId);
|
||||
if (!result.ok) {
|
||||
showNotification(result.error || "Не удалось проверить права", "error");
|
||||
if (!silent) showNotification(result.error || "Не удалось проверить права", "error");
|
||||
return;
|
||||
}
|
||||
setInviteAccessStatus(result.result || []);
|
||||
setTaskNotice({ text: "Проверка прав инвайта завершена.", tone: "success", source });
|
||||
if (!silent) setTaskNotice({ text: "Проверка прав инвайта завершена.", tone: "success", source });
|
||||
} catch (error) {
|
||||
showNotification(error.message || String(error), "error");
|
||||
if (!silent) showNotification(error.message || String(error), "error");
|
||||
}
|
||||
};
|
||||
|
||||
@ -1728,6 +1815,12 @@ export default function App() {
|
||||
<div className="live-label">Запущено</div>
|
||||
<div className="summary-value">{taskSummary.running}</div>
|
||||
</div>
|
||||
<div className="summary-card">
|
||||
<div className="live-label">Сессии</div>
|
||||
<div className="summary-value">
|
||||
{globalStatus.connectedSessions}/{globalStatus.totalAccounts}
|
||||
</div>
|
||||
</div>
|
||||
<div className="summary-card">
|
||||
<div className="live-label">Очередь</div>
|
||||
<div className="summary-value">{taskSummary.queue}</div>
|
||||
@ -1974,13 +2067,37 @@ export default function App() {
|
||||
</div>
|
||||
<div className="status-actions">
|
||||
{taskStatus.running ? (
|
||||
<button className="danger" onClick={() => stopTask("status")} disabled={!hasSelectedTask}>Остановить</button>
|
||||
<button className="danger" onClick={() => stopTask("status")} disabled={!hasSelectedTask || taskActionLoading}>
|
||||
{taskActionLoading ? "Остановка..." : "Остановить"}
|
||||
</button>
|
||||
) : (
|
||||
<button className="primary" onClick={() => startTask("status")} disabled={!hasSelectedTask}>Запустить</button>
|
||||
<button className="primary" onClick={() => startTask("status")} disabled={!hasSelectedTask || taskActionLoading}>
|
||||
{taskActionLoading ? "Запуск..." : "Запустить"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{hasSelectedTask && (
|
||||
<div className="status-text">
|
||||
{taskStatus.readiness && (
|
||||
<div className={`notice inline ${taskStatus.readiness.ok ? "success" : "warn"}`}>
|
||||
Готовность: {taskStatus.readiness.ok ? "Да" : "Нет"}
|
||||
{!taskStatus.readiness.ok && taskStatus.readiness.reasons && (
|
||||
<div className="pre-line">{taskStatus.readiness.reasons.join("\n")}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{roleSummary.invite.length > 0 && (
|
||||
<div className="status-text compact">
|
||||
Лимиты аккаунтов: {roleSummary.invite.map((id) => {
|
||||
const account = accountById.get(id);
|
||||
const stats = accountStatsMap.get(id);
|
||||
if (!account || !stats) return null;
|
||||
const label = account.phone || account.user_id || id;
|
||||
const remaining = stats.remainingToday != null ? stats.remainingToday : "—";
|
||||
return `${label} (${remaining})`;
|
||||
}).filter(Boolean).join(", ")}
|
||||
</div>
|
||||
)}
|
||||
{taskStatus.warnings && taskStatus.warnings.length > 0 && (
|
||||
<div className="notice inline warn">
|
||||
{taskStatus.warnings.map((warning, index) => (
|
||||
@ -2369,7 +2486,7 @@ export default function App() {
|
||||
</div>
|
||||
<div className="side-stat">
|
||||
<span>Аккаунты</span>
|
||||
<strong>{selectedAccountIds.length}</strong>
|
||||
<strong>{assignedAccountCount}</strong>
|
||||
</div>
|
||||
<div className="side-stat">
|
||||
<span>Очередь</span>
|
||||
@ -2391,15 +2508,24 @@ export default function App() {
|
||||
</div>
|
||||
{sidebarExpanded && (
|
||||
<div className="sidebar-actions expanded">
|
||||
<button className="secondary" onClick={() => saveTask("sidebar")} disabled={!canSaveTask}>Сохранить задачу</button>
|
||||
<button className="secondary" onClick={() => parseHistory("sidebar")} disabled={!hasSelectedTask}>Собрать историю</button>
|
||||
<button className="secondary" onClick={() => checkAccess("sidebar")} disabled={!hasSelectedTask}>Проверить доступ</button>
|
||||
<button className="secondary" onClick={() => checkInviteAccess("sidebar")} disabled={!hasSelectedTask}>Проверить права инвайта</button>
|
||||
<button className="secondary" onClick={() => refreshMembership("sidebar")} disabled={!hasSelectedTask}>Проверить участие</button>
|
||||
<button className="secondary" onClick={() => clearQueue("sidebar")} disabled={!hasSelectedTask}>Очистить очередь</button>
|
||||
{accessStatus.length > 0 && (
|
||||
<div className="access-block">
|
||||
<div className="access-title">Доступ к группам</div>
|
||||
<button className="secondary" onClick={() => saveTask("sidebar")} disabled={!canSaveTask}>Сохранить задачу</button>
|
||||
<button className="secondary" onClick={() => parseHistory("sidebar")} disabled={!hasSelectedTask}>Собрать историю</button>
|
||||
<button className="secondary" onClick={() => checkAccess("sidebar")} disabled={!hasSelectedTask}>Проверить доступ</button>
|
||||
<button className="secondary" onClick={() => checkInviteAccess("sidebar")} disabled={!hasSelectedTask}>Проверить права инвайта</button>
|
||||
<button className="secondary" onClick={() => refreshMembership("sidebar")} disabled={!hasSelectedTask}>Проверить участие</button>
|
||||
<button className="secondary" onClick={() => clearQueue("sidebar")} disabled={!hasSelectedTask}>Очистить очередь</button>
|
||||
{taskStatus.running ? (
|
||||
<button className="danger" onClick={() => stopTask("sidebar")} disabled={!hasSelectedTask || taskActionLoading}>
|
||||
{taskActionLoading ? "Остановка..." : "Остановить"}
|
||||
</button>
|
||||
) : (
|
||||
<button className="primary" onClick={() => startTask("sidebar")} disabled={!hasSelectedTask || taskActionLoading}>
|
||||
{taskActionLoading ? "Запуск..." : "Запустить"}
|
||||
</button>
|
||||
)}
|
||||
{accessStatus.length > 0 && (
|
||||
<div className="access-block">
|
||||
<div className="access-title">Доступ к группам</div>
|
||||
<div className="access-list">
|
||||
{accessStatus.map((item, index) => (
|
||||
<div key={`${item.value}-${index}`} className={`access-row ${item.ok ? "ok" : "fail"}`}>
|
||||
@ -2418,6 +2544,9 @@ export default function App() {
|
||||
{inviteAccessStatus.length > 0 && (
|
||||
<div className="access-block">
|
||||
<div className="access-title">Права инвайта</div>
|
||||
<div className="access-subtitle">
|
||||
Проверяются аккаунты с ролью инвайта: {roleSummary.invite.length}
|
||||
</div>
|
||||
<div className="access-list">
|
||||
{inviteAccessStatus.map((item, index) => (
|
||||
<div key={`${item.accountId}-${index}`} className={`access-row ${item.canInvite ? "ok" : "fail"}`}>
|
||||
|
||||
@ -188,6 +188,12 @@ body {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.access-subtitle {
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.access-status {
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
@ -32,6 +32,15 @@ function SettingsTab({ settings, onSettingsChange, settingsNotice, saveSettings
|
||||
onChange={(event) => onSettingsChange("floodCooldownMinutes", Number(event.target.value))}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span className="label-line">Хранить очередь (часы)</span>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={settings.queueTtlHours}
|
||||
onChange={(event) => onSettingsChange("queueTtlHours", Number(event.target.value))}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="row-inline">
|
||||
<button className="secondary" onClick={saveSettings}>Сохранить настройки</button>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user