some
This commit is contained in:
parent
303755f221
commit
d5d2a84a2e
@ -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"
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,8 +2767,17 @@ export default function App() {
|
||||
onChange={(event) => setTaskForm({ ...taskForm, warmupEnabled: event.target.checked })}
|
||||
/>
|
||||
Разогрев лимита
|
||||
<span className="hint">Плавно увеличивает дневной лимит по дням.</span>
|
||||
<span
|
||||
className="hint"
|
||||
title="График: дни 1–3 — 1/д; 4–7 — 2/д; 8–12 — 3/д; 13–18 — 4/д; 19–25 — 5/д; 26–33 — 6/д; с 34-го — 7/д. Итоговый лимит не превышает дневной лимит задачи."
|
||||
>
|
||||
Плавно увеличивает дневной лимит по дням.
|
||||
</span>
|
||||
<span className="hint">Нужен для “прогрева” новых аккаунтов и снижения риска флуда.</span>
|
||||
<span className="hint">График: 1/д ×3, 2/д ×4, 3/д ×5, 4/д ×6, 5/д ×7, 6/д ×8, далее 7/д.</span>
|
||||
</label>
|
||||
{!taskForm.warmupEnabled && (
|
||||
<>
|
||||
<label>
|
||||
<span className="label-line">Стартовый лимит</span>
|
||||
<input
|
||||
@ -2780,7 +2792,6 @@ export default function App() {
|
||||
const value = Number(taskForm.warmupStartLimit);
|
||||
setTaskForm({ ...taskForm, warmupStartLimit: Number.isFinite(value) && value > 0 ? value : 1 });
|
||||
}}
|
||||
disabled={!taskForm.warmupEnabled}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
@ -2797,9 +2808,10 @@ export default function App() {
|
||||
const value = Number(taskForm.warmupDailyIncrease);
|
||||
setTaskForm({ ...taskForm, warmupDailyIncrease: Number.isFinite(value) && value >= 0 ? value : 0 });
|
||||
}}
|
||||
disabled={!taskForm.warmupEnabled}
|
||||
/>
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</details>
|
||||
<details className="section">
|
||||
|
||||
@ -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%;
|
||||
}
|
||||
|
||||
@ -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 || "—");
|
||||
})()}</div>
|
||||
{invite.skippedReason && invite.skippedReason !== "" && (
|
||||
<div className="log-errors">Пропуск: {invite.skippedReason}</div>
|
||||
<div className="log-errors">Результат: {formatErrorWithExplain(invite.skippedReason)}</div>
|
||||
)}
|
||||
{invite.error && invite.error !== "" && (
|
||||
<div className="log-errors">Причина: {invite.error}</div>
|
||||
)}
|
||||
{invite.error && (
|
||||
<div className="log-users">
|
||||
Вероятная причина: {explainInviteError(invite.error) || "Причина не определена"}
|
||||
</div>
|
||||
<div className="log-errors">Ошибка: {formatErrorWithExplain(invite.error)}</div>
|
||||
)}
|
||||
{invite.confirmError && (
|
||||
<div className="log-errors">
|
||||
Подтверждение: {invite.confirmError}
|
||||
Проверка участия: {formatErrorWithExplain(invite.confirmError)}
|
||||
</div>
|
||||
)}
|
||||
{invite.strategy && (
|
||||
@ -478,14 +486,14 @@ function LogsTab({
|
||||
<div>Тип цели: {formatTargetType(invite.targetType)}</div>
|
||||
<div>Действие: {invite.action || "invite"}</div>
|
||||
<div>Статус: {formatInviteStatus(invite.status)}</div>
|
||||
<div>Пропуск: {invite.skippedReason || "—"}</div>
|
||||
<div>Ошибка: {invite.error || "—"}</div>
|
||||
<div>Подтверждение: {invite.confirmError || "—"}</div>
|
||||
<div>Результат: {formatErrorWithExplain(invite.skippedReason)}</div>
|
||||
<div>Ошибка: {formatErrorWithExplain(invite.error)}</div>
|
||||
<div>Проверка участия: {formatErrorWithExplain(invite.confirmError)}</div>
|
||||
{invite.watcherAccountId && invite.accountId && invite.watcherAccountId !== invite.accountId
|
||||
&& selectedTask && selectedTask.randomAccounts && hasBothRoles(invite.watcherAccountId) && (
|
||||
<div>Примечание: у наблюдателя стоят обе роли, но включен случайный выбор — инвайт выполнен другим аккаунтом.</div>
|
||||
)}
|
||||
<div>Вероятная причина: {explainInviteError(invite.error) || "Причина не определена"}</div>
|
||||
<div>Пояснение: {explainRawError(invite.error) || explainRawError(invite.confirmError) || "Причина не определена"}</div>
|
||||
<div>Стратегия: {invite.strategy || "—"}</div>
|
||||
<div className="pre-line">
|
||||
{invite.strategyMeta ? `Стратегии:\n${formatStrategies(invite.strategyMeta)}` : "Стратегии: —"}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user