This commit is contained in:
Ivan Neplokhov 2026-02-06 23:56:38 +04:00
parent a3aac40a73
commit 3e812b2835
7 changed files with 131 additions and 11 deletions

View File

@ -58,12 +58,23 @@ const startTaskWithChecks = async (id) => {
const filteredResult = filterTaskRolesByAccounts(id, taskAccounts, existingAccounts); const filteredResult = filterTaskRolesByAccounts(id, taskAccounts, existingAccounts);
const filteredRoles = filteredResult.filtered; const filteredRoles = filteredResult.filtered;
let adminPrepPartialWarning = ""; let adminPrepPartialWarning = "";
const accountsById = new Map(existingAccounts.map((acc) => [acc.id, acc]));
const isAccountAvailable = (account) => {
if (!account || account.status !== "ok") return false;
if (!account.cooldown_until) return true;
try {
return new Date(account.cooldown_until).getTime() <= Date.now();
} catch {
return true;
}
};
const inviteIds = filteredRoles const inviteIds = filteredRoles
.filter((row) => row.roleInvite && Number(row.inviteLimit || 0) > 0) .filter((row) => row.roleInvite && Number(row.inviteLimit || 0) > 0)
.map((row) => row.accountId); .map((row) => row.accountId)
.filter((id) => isAccountAvailable(accountsById.get(id)));
const monitorIds = filteredRoles.filter((row) => row.roleMonitor).map((row) => row.accountId); const monitorIds = filteredRoles.filter((row) => row.roleMonitor).map((row) => row.accountId);
if (!inviteIds.length) { if (!inviteIds.length) {
return { ok: false, error: "Нет аккаунтов с ролью инвайта." }; return { ok: false, error: "Нет доступных аккаунтов с ролью инвайта (все в ограничении/спаме)." };
} }
if (!monitorIds.length) { if (!monitorIds.length) {
return { ok: false, error: "Нет аккаунтов с ролью мониторинга." }; return { ok: false, error: "Нет аккаунтов с ролью мониторинга." };

View File

@ -1048,8 +1048,13 @@ function initStore(userDataPath) {
LIMIT 1 LIMIT 1
`).get(taskId || 0, String(userId || "")); `).get(taskId || 0, String(userId || ""));
if (!row || !row.id) return; if (!row || !row.id) return;
db.prepare("UPDATE invites SET confirmed = ?, confirm_error = ? WHERE id = ?") if (confirmed) {
.run(confirmed ? 1 : 0, confirmError || "", row.id); db.prepare("UPDATE invites SET confirmed = 1, confirm_error = '', status = 'success' WHERE id = ?")
.run(row.id);
return;
}
db.prepare("UPDATE invites SET confirmed = 0, confirm_error = ? WHERE id = ?")
.run(confirmError || "", row.id);
} }
function listFallback(limit, taskId) { function listFallback(limit, taskId) {

View File

@ -2161,6 +2161,19 @@ class TelegramManager {
this.store.addAccountEvent(masterAccountId, masterAccount.phone || "", "admin_grant_detail", `${diagParts.join(" | ")} | error=session_not_connected`); this.store.addAccountEvent(masterAccountId, masterAccount.phone || "", "admin_grant_detail", `${diagParts.join(" | ")} | error=session_not_connected`);
continue; continue;
} }
if (targetEntry.account && (targetEntry.account.status !== "ok" || this._isInCooldown(targetEntry.account))) {
const reason = targetEntry.account.status !== "ok"
? "Аккаунт в спаме/ограничении"
: "Аккаунт в FLOODкулдауне";
results.push({ accountId, ok: false, reason });
this.store.addAccountEvent(
masterAccountId,
masterAccount.phone || "",
"admin_grant_detail",
`${diagParts.join(" | ")} | skip=${reason}`
);
continue;
}
const targetAccess = await this._resolveGroupEntity( const targetAccess = await this._resolveGroupEntity(
targetEntry.client, targetEntry.client,
task.our_group, task.our_group,
@ -2539,7 +2552,9 @@ class TelegramManager {
async joinGroupsForTask(task, competitorGroups, accountIds, roleIds = {}, options = {}) { async joinGroupsForTask(task, competitorGroups, accountIds, roleIds = {}, options = {}) {
const forceJoin = Boolean(options.forceJoin); const forceJoin = Boolean(options.forceJoin);
const accounts = Array.from(this.clients.values()).filter((entry) => accountIds.includes(entry.account.id)); const accounts = Array.from(this.clients.values())
.filter((entry) => accountIds.includes(entry.account.id))
.filter((entry) => entry.account.status === "ok" && !this._isInCooldown(entry.account));
const competitorBots = Math.max(1, Number(task.max_competitor_bots || 1)); const competitorBots = Math.max(1, Number(task.max_competitor_bots || 1));
const ourBots = Math.max(1, Number(task.max_our_bots || 1)); const ourBots = Math.max(1, Number(task.max_our_bots || 1));

View File

@ -1,3 +1,5 @@
import { useEffect, useRef } from "react";
export default function useAccountManagement({ export default function useAccountManagement({
selectedTaskId, selectedTaskId,
taskAccountRoles, taskAccountRoles,
@ -16,6 +18,17 @@ export default function useAccountManagement({
refreshMembership refreshMembership
}) { }) {
const DEFAULT_INVITE_LIMIT = 7; const DEFAULT_INVITE_LIMIT = 7;
const lastAutoRedistributeRef = useRef(0);
const isAccountAvailable = (account) => {
if (!account || account.status !== "ok") return false;
if (!account.cooldown_until) return true;
try {
return new Date(account.cooldown_until).getTime() <= Date.now();
} catch (error) {
return true;
}
};
const persistAccountRoles = async (next) => { const persistAccountRoles = async (next) => {
if (!window.api || selectedTaskId == null) return; if (!window.api || selectedTaskId == null) return;
@ -97,9 +110,11 @@ export default function useAccountManagement({
const applyRolePreset = async (type) => { const applyRolePreset = async (type) => {
if (!hasSelectedTask) return; if (!hasSelectedTask) return;
const availableIds = selectedAccountIds.length const accountMap = new Map((accounts || []).map((account) => [account.id, account]));
const baseIds = selectedAccountIds.length
? selectedAccountIds ? selectedAccountIds
: accountBuckets.freeOrSelected.map((account) => account.id); : accountBuckets.freeOrSelected.map((account) => account.id);
const availableIds = baseIds.filter((id) => isAccountAvailable(accountMap.get(id)));
if (!availableIds.length) { if (!availableIds.length) {
showNotification("Нет доступных аккаунтов для назначения.", "error"); showNotification("Нет доступных аккаунтов для назначения.", "error");
return; return;
@ -274,6 +289,42 @@ export default function useAccountManagement({
} }
}; };
useEffect(() => {
if (!hasSelectedTask || taskForm.rolesMode !== "auto") return;
const accountMap = new Map((accounts || []).map((account) => [account.id, account]));
const badAssigned = Object.keys(taskAccountRoles).filter((id) => {
const account = accountMap.get(Number(id));
return account && !isAccountAvailable(account);
});
if (!badAssigned.length) return;
const now = Date.now();
if (now - lastAutoRedistributeRef.current < 5000) return;
lastAutoRedistributeRef.current = now;
showNotification("Обнаружены аккаунты в ограничении. Выполняем перераспределение ролей.", "warn");
if (window.api) {
const labels = badAssigned
.map((id) => {
const account = accountMap.get(Number(id));
if (!account) return String(id);
return account.phone || account.username || account.id;
})
.join(", ");
window.api.addAccountEvent({
accountId: 0,
phone: "",
action: "roles_autoredistribute",
details: `задача ${selectedTaskId}: авто‑перераспределение из‑за статуса/лимита (исключены: ${labels || "—"})`
});
}
applyRolePreset(taskForm.requireSameBotInBoth ? "one" : "split");
}, [
accounts,
taskAccountRoles,
hasSelectedTask,
taskForm.rolesMode,
taskForm.requireSameBotInBoth
]);
const moveAccountToTask = async (accountId) => { const moveAccountToTask = async (accountId) => {
if (!window.api || selectedTaskId == null) return; if (!window.api || selectedTaskId == null) return;
await assignAccountsToTask([accountId]); await assignAccountsToTask([accountId]);

View File

@ -82,11 +82,26 @@ export default function useTaskActions({
let accountRolesMap = { ...taskAccountRoles }; let accountRolesMap = { ...taskAccountRoles };
let accountIds = Object.keys(accountRolesMap).map((id) => Number(id)); let accountIds = Object.keys(accountRolesMap).map((id) => Number(id));
const autoRoleMode = nextForm.rolesMode === "auto"; const autoRoleMode = nextForm.rolesMode === "auto";
const eligibleAccounts = (accounts || []).filter((account) => {
if (!account || account.status !== "ok") return false;
if (!account.cooldown_until) return true;
try {
return new Date(account.cooldown_until).getTime() <= Date.now();
} catch {
return true;
}
});
const eligibleIds = eligibleAccounts.map((account) => account.id);
if (autoRoleMode && nextForm.requireSameBotInBoth) { if (autoRoleMode && nextForm.requireSameBotInBoth) {
const required = Math.max(1, Number(nextForm.maxCompetitorBots || 1)); const required = Math.max(1, Number(nextForm.maxCompetitorBots || 1));
const pool = (selectedAccountIds && selectedAccountIds.length ? selectedAccountIds : accounts.map((account) => account.id)) const pool = (selectedAccountIds && selectedAccountIds.length ? selectedAccountIds : eligibleIds)
.filter((id) => Number.isFinite(id)); .filter((id) => Number.isFinite(id));
const chosen = pool.slice(0, required); const filteredPool = pool.filter((id) => eligibleIds.includes(id));
if (!filteredPool.length) {
showNotification("Нет доступных аккаунтов (все в ограничении).", "error");
return;
}
const chosen = filteredPool.slice(0, required);
accountRolesMap = {}; accountRolesMap = {};
chosen.forEach((accountId) => { chosen.forEach((accountId) => {
const existing = taskAccountRoles[accountId] || {}; const existing = taskAccountRoles[accountId] || {};
@ -97,7 +112,11 @@ export default function useTaskActions({
setSelectedAccountIds(chosen); setSelectedAccountIds(chosen);
} }
if (autoRoleMode && nextForm.autoAssignAccounts && (!accountIds || accountIds.length === 0)) { if (autoRoleMode && nextForm.autoAssignAccounts && (!accountIds || accountIds.length === 0)) {
accountIds = accounts.map((account) => account.id); accountIds = eligibleIds;
if (!accountIds.length) {
showNotification("Нет доступных аккаунтов (все в ограничении).", "error");
return;
}
accountRolesMap = {}; accountRolesMap = {};
accountIds.forEach((accountId) => { accountIds.forEach((accountId) => {
const existing = taskAccountRoles[accountId] || {}; const existing = taskAccountRoles[accountId] || {};

View File

@ -26,11 +26,24 @@ export default function useTaskPresets({
showNotification("Нет доступных аккаунтов.", "error"); showNotification("Нет доступных аккаунтов.", "error");
return; return;
} }
const masterId = accounts[0].id; const eligibleAccounts = accounts.filter((account) => {
if (!account || account.status !== "ok") return false;
if (!account.cooldown_until) return true;
try {
return new Date(account.cooldown_until).getTime() <= Date.now();
} catch {
return true;
}
});
if (!eligibleAccounts.length) {
showNotification("Нет доступных аккаунтов (все в ограничении).", "error");
return;
}
const masterId = eligibleAccounts[0].id;
const requiredCount = 3; const requiredCount = 3;
const baseIds = selectedAccountIds.length >= requiredCount const baseIds = selectedAccountIds.length >= requiredCount
? selectedAccountIds.slice() ? selectedAccountIds.slice()
: accounts.map((account) => account.id); : eligibleAccounts.map((account) => account.id);
if (baseIds.length < 3) { if (baseIds.length < 3) {
showNotification("Для раздельных ролей желательно минимум 3 аккаунта (мониторинг/инвайт/подтверждение).", "info"); showNotification("Для раздельных ролей желательно минимум 3 аккаунта (мониторинг/инвайт/подтверждение).", "info");
} }

View File

@ -202,6 +202,9 @@ function AccountsTab({
<div> <div>
<div className="account-phone">{formatAccountLabel(account)}</div> <div className="account-phone">{formatAccountLabel(account)}</div>
<div className={`status ${account.status}`}>{formatAccountStatus(account.status)}</div> <div className={`status ${account.status}`}>{formatAccountStatus(account.status)}</div>
{account.status && account.status !== "ok" && (
<span className="task-badge warn pending-badge">в спаме</span>
)}
<div className={`account-status-pill ${selected ? "busy" : "free"}`}> <div className={`account-status-pill ${selected ? "busy" : "free"}`}>
{selected ? "В задаче" : "Свободен"} {selected ? "В задаче" : "Свободен"}
</div> </div>
@ -408,6 +411,9 @@ function AccountsTab({
<div> <div>
<div className="account-phone">{formatAccountLabel(account)}</div> <div className="account-phone">{formatAccountLabel(account)}</div>
<div className={`status ${account.status}`}>{formatAccountStatus(account.status)}</div> <div className={`status ${account.status}`}>{formatAccountStatus(account.status)}</div>
{account.status && account.status !== "ok" && (
<span className="task-badge warn pending-badge">в спаме</span>
)}
<div className="account-status-pill busy"> <div className="account-status-pill busy">
Занят Занят
<button <button