fix desktop tray icon telegram topics and checkin intervals
This commit is contained in:
@@ -10,7 +10,7 @@ vi.mock('../../services/checkinService.js', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('../../services/checkinScheduler.js', () => ({
|
||||
updateCheckinCron: vi.fn(),
|
||||
updateCheckinSchedule: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../db/index.js', () => {
|
||||
@@ -87,4 +87,25 @@ describe('POST /api/checkin/trigger background task dedupe', () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it('accepts the legacy cron-only schedule payload', async () => {
|
||||
const { checkinRoutes } = await import('./checkin.js');
|
||||
const schedulerModule = await import('../../services/checkinScheduler.js');
|
||||
const app = Fastify();
|
||||
await app.register(checkinRoutes);
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'PUT',
|
||||
url: '/api/checkin/schedule',
|
||||
payload: { cron: '0 8 * * *' },
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect((schedulerModule as any).updateCheckinSchedule).toHaveBeenCalledWith({
|
||||
mode: 'cron',
|
||||
cronExpr: '0 8 * * *',
|
||||
intervalHours: undefined,
|
||||
});
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import { config } from '../../config.js';
|
||||
import { db, schema } from '../../db/index.js';
|
||||
import { upsertSetting } from '../../db/upsertSetting.js';
|
||||
import { eq, desc } from 'drizzle-orm';
|
||||
import { checkinAccount, checkinAll } from '../../services/checkinService.js';
|
||||
import { updateCheckinCron } from '../../services/checkinScheduler.js';
|
||||
import { updateCheckinSchedule } from '../../services/checkinScheduler.js';
|
||||
import { startBackgroundTask, summarizeCheckinResults } from '../../services/backgroundTaskService.js';
|
||||
import { classifyFailureReason } from '../../services/failureReasonService.js';
|
||||
|
||||
@@ -84,7 +85,7 @@ export async function checkinRoutes(app: FastifyInstance) {
|
||||
failureMessage: (currentTask) => `全部账号签到任务失败:${currentTask.error || 'unknown error'}`,
|
||||
},
|
||||
async () => {
|
||||
const results = await checkinAll();
|
||||
const results = await checkinAll({ scheduleMode: config.checkinScheduleMode });
|
||||
return {
|
||||
summary: summarizeCheckinResults(results),
|
||||
total: results.length,
|
||||
@@ -108,7 +109,7 @@ export async function checkinRoutes(app: FastifyInstance) {
|
||||
// Trigger check-in for a specific account
|
||||
app.post<{ Params: { id: string } }>('/api/checkin/trigger/:id', async (request) => {
|
||||
const id = parseInt(request.params.id, 10);
|
||||
const result = await checkinAccount(id);
|
||||
const result = await checkinAccount(id, { scheduleMode: config.checkinScheduleMode });
|
||||
return result;
|
||||
});
|
||||
|
||||
@@ -142,11 +143,33 @@ export async function checkinRoutes(app: FastifyInstance) {
|
||||
});
|
||||
|
||||
// Update check-in schedule
|
||||
app.put<{ Body: { cron: string } }>('/api/checkin/schedule', async (request) => {
|
||||
app.put<{ Body: { mode?: 'cron' | 'interval'; cron?: string; intervalHours?: number } }>('/api/checkin/schedule', async (request) => {
|
||||
try {
|
||||
updateCheckinCron(request.body.cron);
|
||||
await upsertSetting('checkin_cron', request.body.cron);
|
||||
return { success: true, cron: request.body.cron };
|
||||
const body = request.body || {};
|
||||
const nextMode: 'cron' | 'interval' = body.mode === 'interval' ? 'interval' : 'cron';
|
||||
const nextCron = typeof body.cron === 'string' ? body.cron : undefined;
|
||||
const nextIntervalHours = body.intervalHours !== undefined ? Number(body.intervalHours) : undefined;
|
||||
const normalizedIntervalHours = typeof nextIntervalHours === 'number' && Number.isFinite(nextIntervalHours)
|
||||
? Math.trunc(nextIntervalHours)
|
||||
: undefined;
|
||||
|
||||
updateCheckinSchedule({
|
||||
mode: nextMode,
|
||||
cronExpr: nextCron,
|
||||
intervalHours: normalizedIntervalHours,
|
||||
});
|
||||
|
||||
await upsertSetting('checkin_schedule_mode', nextMode);
|
||||
if (nextCron !== undefined) await upsertSetting('checkin_cron', nextCron);
|
||||
if (normalizedIntervalHours !== undefined) {
|
||||
await upsertSetting('checkin_interval_hours', normalizedIntervalHours);
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
mode: nextMode,
|
||||
cron: nextCron,
|
||||
intervalHours: normalizedIntervalHours,
|
||||
};
|
||||
} catch (err: any) {
|
||||
return { error: err.message };
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@ describe('settings and auth events', () => {
|
||||
config.proxyToken = 'sk-old-proxy-token-123';
|
||||
config.systemProxyUrl = '';
|
||||
config.checkinCron = '0 8 * * *';
|
||||
(config as any).checkinScheduleMode = 'cron';
|
||||
(config as any).checkinIntervalHours = 6;
|
||||
config.balanceRefreshCron = '0 * * * *';
|
||||
config.logCleanupConfigured = false;
|
||||
config.logCleanupCron = '0 6 * * *';
|
||||
@@ -56,6 +58,7 @@ describe('settings and auth events', () => {
|
||||
(config as any).telegramBotToken = '';
|
||||
(config as any).telegramChatId = '';
|
||||
(config as any).telegramUseSystemProxy = false;
|
||||
(config as any).telegramMessageThreadId = '';
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -86,6 +89,28 @@ describe('settings and auth events', () => {
|
||||
expect(events[0].message || '').toContain('签到 Cron');
|
||||
});
|
||||
|
||||
it('persists and returns checkin interval mode from runtime settings', async () => {
|
||||
const updateResponse = await app.inject({
|
||||
method: 'PUT',
|
||||
url: '/api/settings/runtime',
|
||||
payload: {
|
||||
checkinScheduleMode: 'interval',
|
||||
checkinIntervalHours: 8,
|
||||
checkinCron: '0 8 * * *',
|
||||
},
|
||||
});
|
||||
|
||||
expect(updateResponse.statusCode).toBe(200);
|
||||
const updated = updateResponse.json() as { checkinScheduleMode?: string; checkinIntervalHours?: number };
|
||||
expect(updated.checkinScheduleMode).toBe('interval');
|
||||
expect(updated.checkinIntervalHours).toBe(8);
|
||||
|
||||
const savedMode = await db.select().from(schema.settings).where(eq(schema.settings.key, 'checkin_schedule_mode')).get();
|
||||
const savedInterval = await db.select().from(schema.settings).where(eq(schema.settings.key, 'checkin_interval_hours')).get();
|
||||
expect(savedMode?.value).toBe(JSON.stringify('interval'));
|
||||
expect(savedInterval?.value).toBe(JSON.stringify(8));
|
||||
});
|
||||
|
||||
it('returns current recognized admin IP in runtime settings response', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
@@ -203,6 +228,32 @@ describe('settings and auth events', () => {
|
||||
expect(runtime.telegramApiBaseUrl).toBe('https://tg-proxy.example.com/custom');
|
||||
});
|
||||
|
||||
it('persists and returns telegram message thread id from runtime settings', async () => {
|
||||
const updateResponse = await app.inject({
|
||||
method: 'PUT',
|
||||
url: '/api/settings/runtime',
|
||||
payload: {
|
||||
telegramMessageThreadId: '77',
|
||||
},
|
||||
});
|
||||
|
||||
expect(updateResponse.statusCode).toBe(200);
|
||||
const updated = updateResponse.json() as { telegramMessageThreadId?: string };
|
||||
expect(updated.telegramMessageThreadId).toBe('77');
|
||||
expect((config as any).telegramMessageThreadId).toBe('77');
|
||||
|
||||
const saved = await db.select().from(schema.settings).where(eq(schema.settings.key, 'telegram_message_thread_id')).get();
|
||||
expect(saved?.value).toBe(JSON.stringify('77'));
|
||||
|
||||
const getResponse = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/settings/runtime',
|
||||
});
|
||||
expect(getResponse.statusCode).toBe(200);
|
||||
const runtime = getResponse.json() as { telegramMessageThreadId?: string };
|
||||
expect(runtime.telegramMessageThreadId).toBe('77');
|
||||
});
|
||||
|
||||
it('rejects invalid telegram api base url when telegram is enabled', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'PUT',
|
||||
|
||||
@@ -4,7 +4,7 @@ import { config } from '../../config.js';
|
||||
import { db, runtimeDbDialect, schema } from '../../db/index.js';
|
||||
import { upsertSetting } from '../../db/upsertSetting.js';
|
||||
import { refreshModelsAndRebuildRoutes } from '../../services/modelService.js';
|
||||
import { updateBalanceRefreshCron, updateCheckinCron, updateLogCleanupSettings } from '../../services/checkinScheduler.js';
|
||||
import { updateBalanceRefreshCron, updateCheckinSchedule, updateLogCleanupSettings } from '../../services/checkinScheduler.js';
|
||||
import { sendNotification } from '../../services/notifyService.js';
|
||||
import { exportBackup, importBackup, type BackupExportType } from '../../services/backupService.js';
|
||||
import { startBackgroundTask } from '../../services/backgroundTaskService.js';
|
||||
@@ -28,6 +28,8 @@ interface RuntimeSettingsBody {
|
||||
proxyToken?: string;
|
||||
systemProxyUrl?: string;
|
||||
checkinCron?: string;
|
||||
checkinScheduleMode?: 'cron' | 'interval';
|
||||
checkinIntervalHours?: number;
|
||||
balanceRefreshCron?: string;
|
||||
logCleanupCron?: string;
|
||||
logCleanupUsageLogsEnabled?: boolean;
|
||||
@@ -44,6 +46,7 @@ interface RuntimeSettingsBody {
|
||||
telegramBotToken?: string;
|
||||
telegramChatId?: string;
|
||||
telegramUseSystemProxy?: boolean;
|
||||
telegramMessageThreadId?: string;
|
||||
smtpEnabled?: boolean;
|
||||
smtpHost?: string;
|
||||
smtpPort?: number;
|
||||
@@ -172,12 +175,46 @@ function normalizeTelegramApiBaseUrl(raw: string): string {
|
||||
return String(raw || '').trim().replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
function normalizeTelegramMessageThreadId(raw: unknown): string {
|
||||
return String(raw || '').trim();
|
||||
}
|
||||
|
||||
function isValidTelegramMessageThreadId(raw: string): boolean {
|
||||
return /^[1-9]\d*$/.test(raw);
|
||||
}
|
||||
|
||||
function applyImportedSettingToRuntime(key: string, value: unknown) {
|
||||
switch (key) {
|
||||
case 'checkin_cron': {
|
||||
if (typeof value !== 'string' || !value || !cron.validate(value)) return;
|
||||
config.checkinCron = value;
|
||||
updateCheckinCron(value);
|
||||
updateCheckinSchedule({
|
||||
mode: config.checkinScheduleMode,
|
||||
cronExpr: config.checkinCron,
|
||||
intervalHours: config.checkinIntervalHours,
|
||||
});
|
||||
return;
|
||||
}
|
||||
case 'checkin_schedule_mode': {
|
||||
if (value !== 'cron' && value !== 'interval') return;
|
||||
const nextMode: 'cron' | 'interval' = value;
|
||||
config.checkinScheduleMode = nextMode;
|
||||
updateCheckinSchedule({
|
||||
mode: config.checkinScheduleMode,
|
||||
cronExpr: config.checkinCron,
|
||||
intervalHours: config.checkinIntervalHours,
|
||||
});
|
||||
return;
|
||||
}
|
||||
case 'checkin_interval_hours': {
|
||||
const intervalHours = Number(value);
|
||||
if (!Number.isFinite(intervalHours) || intervalHours < 1 || intervalHours > 24) return;
|
||||
config.checkinIntervalHours = Math.trunc(intervalHours);
|
||||
updateCheckinSchedule({
|
||||
mode: config.checkinScheduleMode,
|
||||
cronExpr: config.checkinCron,
|
||||
intervalHours: config.checkinIntervalHours,
|
||||
});
|
||||
return;
|
||||
}
|
||||
case 'balance_refresh_cron': {
|
||||
@@ -293,6 +330,11 @@ function applyImportedSettingToRuntime(key: string, value: unknown) {
|
||||
config.telegramUseSystemProxy = !!value;
|
||||
return;
|
||||
}
|
||||
case 'telegram_message_thread_id': {
|
||||
if (typeof value !== 'string') return;
|
||||
config.telegramMessageThreadId = value.trim();
|
||||
return;
|
||||
}
|
||||
case 'smtp_enabled': {
|
||||
config.smtpEnabled = !!value;
|
||||
return;
|
||||
@@ -368,6 +410,8 @@ function applyImportedSettingToRuntime(key: string, value: unknown) {
|
||||
function getRuntimeSettingsResponse(currentAdminIp = '') {
|
||||
return {
|
||||
checkinCron: config.checkinCron,
|
||||
checkinScheduleMode: config.checkinScheduleMode,
|
||||
checkinIntervalHours: config.checkinIntervalHours,
|
||||
balanceRefreshCron: config.balanceRefreshCron,
|
||||
logCleanupCron: config.logCleanupCron,
|
||||
logCleanupUsageLogsEnabled: config.logCleanupUsageLogsEnabled,
|
||||
@@ -386,6 +430,7 @@ function getRuntimeSettingsResponse(currentAdminIp = '') {
|
||||
telegramBotTokenMasked: maskSecret(config.telegramBotToken),
|
||||
telegramChatId: config.telegramChatId,
|
||||
telegramUseSystemProxy: config.telegramUseSystemProxy,
|
||||
telegramMessageThreadId: config.telegramMessageThreadId,
|
||||
smtpEnabled: config.smtpEnabled,
|
||||
smtpHost: config.smtpHost,
|
||||
smtpPort: config.smtpPort,
|
||||
@@ -519,7 +564,8 @@ export async function settingsRoutes(app: FastifyInstance) {
|
||||
|| body.telegramApiBaseUrl !== undefined
|
||||
|| body.telegramBotToken !== undefined
|
||||
|| body.telegramChatId !== undefined
|
||||
|| body.telegramUseSystemProxy !== undefined;
|
||||
|| body.telegramUseSystemProxy !== undefined
|
||||
|| body.telegramMessageThreadId !== undefined;
|
||||
const nextTelegramEnabled = body.telegramEnabled !== undefined
|
||||
? !!body.telegramEnabled
|
||||
: config.telegramEnabled;
|
||||
@@ -532,6 +578,9 @@ export async function settingsRoutes(app: FastifyInstance) {
|
||||
const nextTelegramChatId = body.telegramChatId !== undefined
|
||||
? String(body.telegramChatId || '').trim()
|
||||
: config.telegramChatId;
|
||||
const nextTelegramMessageThreadId = body.telegramMessageThreadId !== undefined
|
||||
? normalizeTelegramMessageThreadId(body.telegramMessageThreadId)
|
||||
: config.telegramMessageThreadId;
|
||||
if (telegramTouched && nextTelegramEnabled) {
|
||||
if (!nextTelegramBotToken) {
|
||||
return reply.code(400).send({ success: false, message: 'Telegram Bot Token 不能为空(启用 Telegram 时)' });
|
||||
@@ -542,13 +591,22 @@ export async function settingsRoutes(app: FastifyInstance) {
|
||||
if (!nextTelegramChatId) {
|
||||
return reply.code(400).send({ success: false, message: 'Telegram Chat ID 不能为空(启用 Telegram 时)' });
|
||||
}
|
||||
if (nextTelegramMessageThreadId && !isValidTelegramMessageThreadId(nextTelegramMessageThreadId)) {
|
||||
return reply.code(400).send({ success: false, message: 'Telegram Topic ID 格式无效,需要正整数' });
|
||||
}
|
||||
if (nextTelegramApiBaseUrl && !isValidHttpUrl(nextTelegramApiBaseUrl)) {
|
||||
return reply.code(400).send({ success: false, message: 'Telegram API Base URL 无效,请填写 http/https 地址' });
|
||||
}
|
||||
} else if (body.telegramApiBaseUrl !== undefined && nextTelegramApiBaseUrl && !isValidHttpUrl(nextTelegramApiBaseUrl)) {
|
||||
return reply.code(400).send({ success: false, message: 'Telegram API Base URL 无效,请填写 http/https 地址' });
|
||||
} else if (body.telegramMessageThreadId !== undefined && nextTelegramMessageThreadId && !isValidTelegramMessageThreadId(nextTelegramMessageThreadId)) {
|
||||
return reply.code(400).send({ success: false, message: 'Telegram Topic ID 格式无效,需要正整数' });
|
||||
}
|
||||
|
||||
const checkinScheduleTouched = body.checkinCron !== undefined
|
||||
|| body.checkinScheduleMode !== undefined
|
||||
|| body.checkinIntervalHours !== undefined;
|
||||
|
||||
if (body.checkinCron !== undefined) {
|
||||
if (!cron.validate(body.checkinCron)) {
|
||||
return reply.code(400).send({ success: false, message: '签到 Cron 表达式无效' });
|
||||
@@ -556,8 +614,50 @@ export async function settingsRoutes(app: FastifyInstance) {
|
||||
if (body.checkinCron !== config.checkinCron) {
|
||||
changedLabels.push(`签到 Cron(${config.checkinCron} -> ${body.checkinCron})`);
|
||||
}
|
||||
updateCheckinCron(body.checkinCron);
|
||||
upsertSetting('checkin_cron', body.checkinCron);
|
||||
}
|
||||
|
||||
if (body.checkinScheduleMode !== undefined) {
|
||||
if (body.checkinScheduleMode !== 'cron' && body.checkinScheduleMode !== 'interval') {
|
||||
return reply.code(400).send({ success: false, message: '签到方式无效:仅支持 cron 或 interval' });
|
||||
}
|
||||
if (body.checkinScheduleMode !== config.checkinScheduleMode) {
|
||||
changedLabels.push('签到方式');
|
||||
}
|
||||
config.checkinScheduleMode = body.checkinScheduleMode;
|
||||
}
|
||||
|
||||
if (body.checkinIntervalHours !== undefined) {
|
||||
const intervalHours = Number(body.checkinIntervalHours);
|
||||
if (!Number.isFinite(intervalHours) || intervalHours < 1 || intervalHours > 24) {
|
||||
return reply.code(400).send({ success: false, message: '签到间隔必须是 1 到 24 的整数小时' });
|
||||
}
|
||||
const nextIntervalHours = Math.trunc(intervalHours);
|
||||
if (nextIntervalHours !== config.checkinIntervalHours) {
|
||||
changedLabels.push(`签到间隔(${config.checkinIntervalHours}h -> ${nextIntervalHours}h)`);
|
||||
}
|
||||
config.checkinIntervalHours = nextIntervalHours;
|
||||
}
|
||||
|
||||
if (checkinScheduleTouched) {
|
||||
const nextCheckinCron = body.checkinCron !== undefined ? body.checkinCron : config.checkinCron;
|
||||
const nextCheckinScheduleMode: 'cron' | 'interval' = body.checkinScheduleMode !== undefined
|
||||
? body.checkinScheduleMode
|
||||
: config.checkinScheduleMode;
|
||||
const nextCheckinIntervalHours = body.checkinIntervalHours !== undefined
|
||||
? Math.trunc(Number(body.checkinIntervalHours))
|
||||
: config.checkinIntervalHours;
|
||||
|
||||
updateCheckinSchedule({
|
||||
mode: nextCheckinScheduleMode,
|
||||
cronExpr: nextCheckinCron,
|
||||
intervalHours: nextCheckinIntervalHours,
|
||||
});
|
||||
config.checkinCron = nextCheckinCron;
|
||||
config.checkinScheduleMode = nextCheckinScheduleMode;
|
||||
config.checkinIntervalHours = nextCheckinIntervalHours;
|
||||
upsertSetting('checkin_cron', config.checkinCron);
|
||||
upsertSetting('checkin_schedule_mode', config.checkinScheduleMode);
|
||||
upsertSetting('checkin_interval_hours', config.checkinIntervalHours);
|
||||
}
|
||||
|
||||
if (body.balanceRefreshCron !== undefined) {
|
||||
@@ -783,6 +883,15 @@ export async function settingsRoutes(app: FastifyInstance) {
|
||||
upsertSetting('telegram_use_system_proxy', config.telegramUseSystemProxy);
|
||||
}
|
||||
|
||||
if (body.telegramMessageThreadId !== undefined) {
|
||||
const nextTelegramMessageThreadId = normalizeTelegramMessageThreadId(body.telegramMessageThreadId);
|
||||
if (nextTelegramMessageThreadId !== config.telegramMessageThreadId) {
|
||||
changedLabels.push('Telegram Topic ID');
|
||||
}
|
||||
config.telegramMessageThreadId = nextTelegramMessageThreadId;
|
||||
upsertSetting('telegram_message_thread_id', config.telegramMessageThreadId);
|
||||
}
|
||||
|
||||
if (body.smtpEnabled !== undefined) {
|
||||
if (!!body.smtpEnabled !== config.smtpEnabled) {
|
||||
changedLabels.push('SMTP 开关');
|
||||
|
||||
Reference in New Issue
Block a user