import React from "react";
import HelpTip from "./HelpTip.jsx";
import { getPresetLabel } from "../utils/presetLabels.js";
export default function TaskSettingsTab({
selectedTaskName,
taskForm,
setTaskForm,
activePreset,
setActivePreset,
applyTaskPreset,
formatAccountLabel,
accountById,
competitorText,
setCompetitorText,
roleMode,
applyRoleMode,
normalizeIntervals,
taskStatus,
perAccountInviteSum,
hasSelectedTask,
inviteAccessStatus,
inviteAccessCheckedAt,
confirmAccessStatus,
confirmAccessCheckedAt,
formatTimestamp,
checkInviteAccess,
checkConfirmAccess,
accounts,
showNotification,
copyToClipboard,
sanitizeTaskForm,
hasPerAccountInviteLimits,
fileImportResult,
importInviteFile,
fileImportForm,
setFileImportForm,
criticalErrorAccounts
}) {
const [customPresetName, setCustomPresetName] = React.useState("");
const [customPresets, setCustomPresets] = React.useState(() => {
try {
const raw = localStorage.getItem("tia_custom_presets_v1");
return raw ? JSON.parse(raw) : [];
} catch {
return [];
}
});
const changedSections = (nextForm) => {
const sections = [];
const hasAny = (keys) => keys.some((k) => JSON.stringify(taskForm[k]) !== JSON.stringify(nextForm[k]));
if (hasAny(["name", "ourGroup"])) sections.push("Основное");
if (hasAny(["maxCompetitorBots", "maxOurBots", "separateConfirmRoles", "maxConfirmBots", "rolesMode", "requireSameBotInBoth"])) sections.push("Роли ботов и вступление");
if (hasAny(["inviteViaAdmins", "inviteAdminMasterId", "inviteAdminAnonymous", "inviteAdminAllowFlood", "inviteLinkOnFail"])) sections.push("Инвайт через админов");
if (hasAny(["minIntervalMinutes", "maxIntervalMinutes", "dailyLimit", "maxInvitesPerCycle", "warmupEnabled", "historyLimit"])) sections.push("Интервалы и лимиты");
return sections;
};
const confirmApply = (nextForm, label, applyFn, sectionsOverride = null) => {
const sections = Array.isArray(sectionsOverride) ? sectionsOverride : changedSections(nextForm);
const msg = sections.length
? `Пресет "${label}" изменит текущие настройки.\n\nБудут изменены разделы:\n- ${sections.join("\n- ")}\n\nПрименить?`
: `Пресет "${label}" не меняет текущие настройки.\nПрименить?`;
if (!window.confirm(msg)) return;
applyFn();
};
const saveCustomPreset = () => {
const name = customPresetName.trim();
if (!name) {
showNotification("Введите имя пресета.", "error");
return;
}
const next = [...customPresets.filter((p) => p.name !== name), { name, form: taskForm }];
setCustomPresets(next);
localStorage.setItem("tia_custom_presets_v1", JSON.stringify(next));
setCustomPresetName("");
showNotification(`Сохранен пользовательский пресет: ${name}`, "success");
};
const applyCustomPreset = (preset) => {
const nextForm = sanitizeTaskForm({ ...taskForm, ...preset.form });
confirmApply(nextForm, preset.name, () => {
setTaskForm(nextForm);
setActivePreset(`custom:${preset.name}`);
showNotification(`Применен пользовательский пресет: ${preset.name}`, "success");
});
};
const deleteCustomPreset = (name) => {
const next = customPresets.filter((preset) => preset.name !== name);
setCustomPresets(next);
localStorage.setItem("tia_custom_presets_v1", JSON.stringify(next));
if (activePreset === `custom:${name}`) {
setActivePreset("");
}
showNotification(`Удален пользовательский пресет: ${name}`, "success");
};
const renameCustomPreset = (name) => {
const nextName = window.prompt("Новое имя пресета:", name);
if (!nextName) return;
const trimmed = nextName.trim();
if (!trimmed) return;
const preset = customPresets.find((p) => p.name === name);
if (!preset) return;
const next = [
...customPresets.filter((p) => p.name !== name && p.name !== trimmed),
{ ...preset, name: trimmed }
];
setCustomPresets(next);
localStorage.setItem("tia_custom_presets_v1", JSON.stringify(next));
if (activePreset === `custom:${name}`) {
setActivePreset(`custom:${trimmed}`);
}
showNotification(`Переименован пресет: ${name} → ${trimmed}`, "success");
};
const exportCustomPresets = () => {
try {
const payload = {
version: 1,
exportedAt: new Date().toISOString(),
presets: customPresets
};
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "custom-presets.json";
link.click();
URL.revokeObjectURL(url);
showNotification("Пользовательские пресеты экспортированы.", "success");
} catch (error) {
showNotification(error.message || String(error), "error");
}
};
const importCustomPresets = async (event) => {
const file = event.target.files && event.target.files[0];
event.target.value = "";
if (!file) return;
try {
const text = await file.text();
const parsed = JSON.parse(text);
const list = Array.isArray(parsed) ? parsed : Array.isArray(parsed.presets) ? parsed.presets : [];
const valid = list
.filter((item) => item && typeof item.name === "string" && item.name.trim() && item.form && typeof item.form === "object")
.map((item) => ({ name: item.name.trim(), form: item.form }));
if (!valid.length) {
showNotification("Файл не содержит корректных пресетов.", "error");
return;
}
const replaceAll = window.confirm("Заменить все текущие пользовательские пресеты?\n\nОК — заменить полностью\nОтмена — объединить с текущими");
const map = new Map();
if (!replaceAll) {
customPresets.forEach((preset) => map.set(preset.name, preset));
}
valid.forEach((preset) => map.set(preset.name, preset));
const next = Array.from(map.values());
setCustomPresets(next);
localStorage.setItem("tia_custom_presets_v1", JSON.stringify(next));
showNotification(`Импортировано пресетов: ${valid.length}. Режим: ${replaceAll ? "замена" : "объединение"}.`, "success");
} catch (error) {
showNotification(error.message || String(error), "error");
}
};
const inviteChecks = Array.isArray(inviteAccessStatus) ? inviteAccessStatus : [];
const inviteChecksById = React.useMemo(() => {
const map = new Map();
inviteChecks.forEach((item) => {
map.set(Number(item.accountId), item);
});
return map;
}, [inviteChecks]);
const masterId = Number(taskForm.inviteAdminMasterId || 0);
const masterAccount = masterId ? (accountById.get(masterId) || accounts.find((item) => Number(item.id) === masterId)) : null;
const masterCheck = masterId ? inviteChecksById.get(masterId) : null;
const checkedCount = inviteChecks.length;
const canInviteCount = inviteChecks.filter((item) => item && item.canInvite).length;
const confirmChecks = Array.isArray(confirmAccessStatus) ? confirmAccessStatus : [];
const confirmCheckedCount = confirmChecks.length;
const confirmOkCount = confirmChecks.filter((item) => item && item.ok).length;
const diagnostics = [
{
title: "Режим",
value: taskForm.inviteViaAdmins ? "включен" : "выключен",
tone: taskForm.inviteViaAdmins ? "ok" : "warn"
},
{
title: "Мастер-админ",
value: masterId ? formatAccountLabel(masterAccount || { id: masterId, phone: `ID ${masterId}` }) : "не выбран",
tone: masterId ? "ok" : "fail"
},
{
title: "Проверка прав",
value: checkedCount ? `выполнена (${checkedCount} аккаунтов)` : "не выполнялась",
tone: checkedCount ? "ok" : "warn"
},
{
title: "Права мастера",
value: !masterId
? "нельзя проверить без выбора мастера"
: !checkedCount
? "нет данных (нажмите «Проверить права»)"
: masterCheck
? (masterCheck.canInvite ? "OK: может приглашать" : `ошибка: ${masterCheck.reason || "нет права приглашать"}`)
: "мастер не вошел в результаты проверки",
tone: !masterId ? "warn" : !checkedCount ? "warn" : (masterCheck && masterCheck.canInvite ? "ok" : "fail")
},
{
title: "Инвайтеры с правом",
value: checkedCount ? `${canInviteCount}/${checkedCount}` : "—",
tone: !checkedCount ? "warn" : canInviteCount > 0 ? "ok" : "fail"
}
];
return (
Настройки задачи
Для: {selectedTaskName}
Основное
Пресеты:
confirmApply(taskForm, "Автораспределение + Инвайт через админа", () => applyTaskPreset("admin"), ["Роли ботов и вступление", "Инвайт через админов", "Интервалы и лимиты"])}
>
Автораспределение + Инвайт через админа
confirmApply(taskForm, "Автораспределение + Без админки", () => applyTaskPreset("no_admin"), ["Роли ботов и вступление", "Инвайт через админов", "Интервалы и лимиты"])}
>
Автораспределение + Без админки
confirmApply(taskForm, "Мягкий 50/день (5 инвайтеров)", () => applyTaskPreset("soft_50"), ["Роли ботов и вступление", "Интервалы и лимиты"])}
>
Мягкий 50/день (5 инвайтеров)
confirmApply(taskForm, "Мягкий 25/день (2 инвайтера)", () => applyTaskPreset("soft_25"), ["Роли ботов и вступление", "Интервалы и лимиты"])}
>
Мягкий 25/день (2 инвайтера)
confirmApply(taskForm, "Мягкий 50/день + админы", () => applyTaskPreset("soft_50_admin"), ["Роли ботов и вступление", "Инвайт через админов", "Интервалы и лимиты"])}
>
Мягкий 50/день + админы
confirmApply(taskForm, "Мягкий 25/день + админы", () => applyTaskPreset("soft_25_admin"), ["Роли ботов и вступление", "Инвайт через админов", "Интервалы и лимиты"])}
>
Мягкий 25/день + админы
Мастер-админ: {taskForm.inviteAdminMasterId ? formatAccountLabel(accountById.get(taskForm.inviteAdminMasterId)) : "не выбран"}
Выбранный пресет: {getPresetLabel(activePreset)}.
При применении пресета будут изменены: «Роли ботов и вступление», «Инвайт через админов», «Интервалы и лимиты».
setCustomPresetName(event.target.value)}
placeholder="Имя пользовательского пресета"
/>
Сохранить пресет
Экспорт пресетов
Импорт пресетов
{customPresets.map((preset) => (
applyCustomPreset(preset)}
>
{preset.name}
renameCustomPreset(preset.name)}>Переименовать
deleteCustomPreset(preset.name)}>Удалить
))}
Название задачи *
setTaskForm({ ...taskForm, name: event.target.value })}
placeholder="Например, Таиланд"
/>
Наша группа *
setTaskForm({ ...taskForm, ourGroup: event.target.value })}
placeholder="https://t.me/..."
/>
Группы конкурентов *
Базовые настройки
Роли ботов и вступление
setTaskForm({ ...taskForm, autoJoinCompetitors: event.target.checked })}
/>
Автодобавление аккаунтов в группы конкурентов
setTaskForm({ ...taskForm, autoJoinOurGroup: event.target.checked })}
/>
Автодобавление аккаунтов в нашу группу
applyRoleMode("split")}
/>
Разделить роли (конкуренты и наша группа разными аккаунтами)
applyRoleMode("same")}
/>
Один и тот же бот в конкурентах и нашей группе
Режим “один и тот же бот” нужен, когда аккаунт должен быть в конкурентах и в нашей группе для инвайта.
Интервалы и лимиты
Мин. интервал (мин) *
{
const value = event.target.value;
setTaskForm({ ...taskForm, minIntervalMinutes: value === "" ? "" : Number(value) });
}}
onBlur={() => setTaskForm(normalizeIntervals(taskForm))}
/>
Макс. интервал (мин) *
{
const value = event.target.value;
setTaskForm({ ...taskForm, maxIntervalMinutes: value === "" ? "" : Number(value) });
}}
onBlur={() => setTaskForm(normalizeIntervals(taskForm))}
/>
Лимит в день *
{
const value = event.target.value;
setTaskForm({ ...taskForm, dailyLimit: value === "" ? "" : Number(value) });
}}
onBlur={() => {
const value = Number(taskForm.dailyLimit);
setTaskForm({ ...taskForm, dailyLimit: Number.isFinite(value) && value > 0 ? value : 1 });
}}
/>
Фактический лимит сегодня: {taskStatus.dailyLimit || "—"}
История сообщений (шт) *
{
const value = event.target.value;
setTaskForm({ ...taskForm, historyLimit: value === "" ? "" : Number(value) });
}}
onBlur={() => {
const value = Number(taskForm.historyLimit);
setTaskForm({ ...taskForm, historyLimit: Number.isFinite(value) && value > 0 ? value : 1 });
}}
/>
Инвайтов за цикл *
{
const value = event.target.value;
setTaskForm({ ...taskForm, maxInvitesPerCycle: value === "" ? "" : Number(value) });
}}
onBlur={() => {
const value = Number(taskForm.maxInvitesPerCycle);
setTaskForm({ ...taskForm, maxInvitesPerCycle: Number.isFinite(value) && value > 0 ? value : 1 });
}}
/>
Это общий потолок на цикл. Пер‑аккаунтные лимиты распределяют инвайты внутри этого числа.
Итоговая формула: фактический лимит сегодня = min(дневной лимит задачи, разогрев).
Внутри цикла инвайты распределяются по аккаунтам согласно их лимитам.
Фактический лимит сегодня: {taskStatus.dailyLimit || "—"}
Сумма лимитов по аккаунтам: {perAccountInviteSum || "—"}
setTaskForm({ ...taskForm, warmupEnabled: event.target.checked })}
/>
Разогрев лимита
Плавно увеличивает дневной лимит по дням.
Нужен для “прогрева” новых аккаунтов и снижения риска флуда.
График: 1/д ×3, 2/д ×4, 3/д ×5, 4/д ×6, 5/д ×7, 6/д ×8, далее 7/д.
{taskForm.warmupEnabled && (
<>
Стартовый лимит
{
const value = event.target.value;
setTaskForm({ ...taskForm, warmupStartLimit: value === "" ? "" : Number(value) });
}}
onBlur={() => {
const value = Number(taskForm.warmupStartLimit);
setTaskForm({ ...taskForm, warmupStartLimit: Number.isFinite(value) && value > 0 ? value : 1 });
}}
/>
Стартовый лимит применяется в первый день. Далее лимит увеличивается по графику прогрева: итог = Стартовый лимит + (ступень прогрева − 1), но не выше дневного лимита задачи.
Прирост в день
{
const value = event.target.value;
setTaskForm({ ...taskForm, warmupDailyIncrease: value === "" ? "" : Number(value) });
}}
onBlur={() => {
const value = Number(taskForm.warmupDailyIncrease);
setTaskForm({ ...taskForm, warmupDailyIncrease: Number.isFinite(value) && value >= 0 ? value : 0 });
}}
/>
>
)}
Расширенные настройки
Инвайт через админов
Шаги: 1) Включить режим 2) Выбрать мастер‑админа 3) Проверить права
setTaskForm({ ...taskForm, inviteViaAdmins: event.target.checked })}
/>
Инвайтить через админов
Временно выдаём инвайтеру право “Приглашать”, затем снимаем права.
checkInviteAccess("admin_block")}
disabled={!hasSelectedTask}
>
Проверить права
{inviteAccessStatus && inviteAccessStatus.length
? `Права проверены${inviteAccessCheckedAt ? ` (${formatTimestamp(inviteAccessCheckedAt)})` : ""}${
inviteAccessStatus.every((item) => item.canInvite) ? " · OK" : " · есть ошибки"
}`
: "Нет проверки"}
{taskForm.inviteViaAdmins && !taskForm.inviteAdminMasterId && (
Не выбран мастер‑админ. Инвайт через админов работать не будет.
)}
Главный аккаунт
setTaskForm({ ...taskForm, inviteAdminMasterId: Number(event.target.value) || 0 })}
disabled={!taskForm.inviteViaAdmins}
>
Не выбран
{accounts.map((account) => (
{formatAccountLabel(account)}
))}
{
const account = accountById.get(taskForm.inviteAdminMasterId);
const username = account && account.username ? `@${account.username}` : "";
if (!username) {
showNotification("У выбранного аккаунта нет username.", "error");
return;
}
const ok = await copyToClipboard(username);
showNotification(ok ? `Скопировано: ${username}` : "Не удалось скопировать.", ok ? "success" : "error");
}}
>
Копировать username
Этот аккаунт должен быть админом в целевой группе и уметь выдавать права другим.
setTaskForm({ ...taskForm, inviteAdminAnonymous: event.target.checked })}
disabled={!taskForm.inviteViaAdmins}
/>
Делать админов анонимными
Мастер-админ назначает остальных админами с анонимностью и минимальными правами.
setTaskForm({ ...taskForm, inviteAdminAllowFlood: event.target.checked })}
disabled={!taskForm.inviteViaAdmins}
/>
Инвайтить в чаты с флудом
Использует выдачу прав между аккаунтами, если Telegram ограничивает инвайтинг.
Диагностика мастер-админа
{diagnostics.map((item) => (
{item.title}
{item.value}
))}
Если есть ошибки: проверьте, что мастер-админ в целевой группе, у него есть право выдачи админов, а инвайтеры состоят в целевой группе.
Распределение ботов
{roleMode === "same" ? (
Ботов в обеих группах
{
const value = event.target.value;
if (value === "") {
setTaskForm({ ...taskForm, maxCompetitorBots: "", maxOurBots: "" });
return;
}
const nextValue = Number(value);
const nextForm = { ...taskForm, maxCompetitorBots: nextValue, maxOurBots: nextValue };
setTaskForm(sanitizeTaskForm(nextForm));
}}
onBlur={() => {
const value = Number(taskForm.maxCompetitorBots);
const normalized = Number.isFinite(value) && value > 0 ? value : 1;
setTaskForm(sanitizeTaskForm({ ...taskForm, maxCompetitorBots: normalized, maxOurBots: normalized }));
}}
/>
Одинаковое количество для конкурентов и нашей группы.
{taskForm.rolesMode === "manual" ? " В ручном режиме не используется." : ""}
) : (
<>
Ботов в конкурентах
{
const value = event.target.value;
setTaskForm({
...taskForm,
maxCompetitorBots: value === "" ? "" : Number(value)
});
}}
onBlur={() => {
const value = Number(taskForm.maxCompetitorBots);
setTaskForm({ ...taskForm, maxCompetitorBots: Number.isFinite(value) && value > 0 ? value : 1 });
}}
/>
Используется для авто‑распределения ролей мониторинга и авто‑вступления в группы конкурентов.
В ручном режиме ролей не ограничивает мониторинг.
{taskForm.rolesMode === "manual" ? " В ручном режиме не используется." : ""}
Ботов в нашей группе
{
const value = event.target.value;
setTaskForm({ ...taskForm, maxOurBots: value === "" ? "" : Number(value) });
}}
onBlur={() => {
const value = Number(taskForm.maxOurBots);
setTaskForm({ ...taskForm, maxOurBots: Number.isFinite(value) && value > 0 ? value : 1 });
}}
/>
Используется для авто‑распределения инвайтеров и авто‑вступления в нашу группу.
В ручном режиме ролей инвайт идёт по отмеченным аккаунтам.
{taskForm.rolesMode === "manual" ? " В ручном режиме не используется." : ""}
>
)}
Дополнительные настройки
Не трогайте, если не уверены.
Безопасность
{!hasPerAccountInviteLimits && (
setTaskForm({ ...taskForm, randomAccounts: event.target.checked })}
/>
Случайный выбор аккаунтов
Инвайты распределяются случайно между доступными аккаунтами.
)}
{!hasPerAccountInviteLimits && (
setTaskForm({ ...taskForm, multiAccountsPerRun: event.target.checked })}
/>
Несколько аккаунтов за цикл
Если выключено — в каждом цикле используется один аккаунт.
)}
setTaskForm({ ...taskForm, retryOnFail: event.target.checked })}
/>
Повторять при ошибке
Повторяем до 2 раз при неудачном инвайте.
setTaskForm({ ...taskForm, inviteLinkOnFail: event.target.checked })}
/>
Отправлять ссылку при USER_NOT_MUTUAL_CONTACT
Отправляет пользователю ссылку из поля “Наша группа”.
setTaskForm({ ...taskForm, stopOnBlocked: event.target.checked })}
/>
Останавливать при блокировках
Останавливает задачу, если % ограниченных аккаунтов выше порога.
setTaskForm({ ...taskForm, allowStartWithoutInviteRights: event.target.checked })}
/>
Разрешать запуск без прав инвайта
Полезно, если вы выдаёте админов после автодобавления.
setTaskForm({ ...taskForm, useWatcherInviteNoUsername: event.target.checked })}
/>
Инвайт через наблюдателя, если нет username
Правило 1: сначала резолвим @username в сессии инвайтера — участие в группе конкурентов не требуется.
Правило 2: если username нет — инвайтим наблюдателем, потому что access_hash валиден только в его сессии.
Остановить при блоке, %
{
const value = event.target.value;
setTaskForm({ ...taskForm, stopBlockedPercent: value === "" ? "" : Number(value) });
}}
onBlur={() => {
const value = Number(taskForm.stopBlockedPercent);
setTaskForm({ ...taskForm, stopBlockedPercent: Number.isFinite(value) && value > 0 ? value : 1 });
}}
disabled={!taskForm.stopOnBlocked}
/>
Импорт аудитории необязательно
Используйте только если нужен импорт из файла или полный список участников.
Импортировать файл
{fileImportResult && (
Импортировано: {fileImportResult.importedCount} · Пропущено: {fileImportResult.skippedCount} · Ошибок: {fileImportResult.failed.length}
)}
Дополнительные параметры
setTaskForm({ ...taskForm, parseParticipants: event.target.checked })}
/>
Собирать участников чатов конкурентов
Используется для закрытых участников и полного списка аудитории.
setTaskForm({ ...taskForm, cycleCompetitors: event.target.checked })}
/>
Циклически обходить конкурентов
Мониторинг и сбор будут переключаться по группам по очереди.
setFileImportForm({ ...fileImportForm, onlyIds: event.target.checked })}
/>
В файле только ID
Если включено — нужен источник (чат), из которого брались ID.
Источник для ID
setFileImportForm({ ...fileImportForm, sourceChat: event.target.value })}
disabled={!fileImportForm.onlyIds}
/>
Используется для резолва ID при инвайте.
{fileImportResult && fileImportResult.failed.length > 0 && (
{fileImportResult.failed.map((item, index) => (
))}
)}
Ошибки аккаунтов
{criticalErrorAccounts.length === 0 && (
Ошибок нет.
)}
{criticalErrorAccounts.length > 0 && (
{criticalErrorAccounts.map((account) => (
{formatAccountLabel(account)}
{account.last_error || "Ошибка сессии"}
))}
)}
Заметки
);
}