feat: 添加 Telegram 通知支持,更新配置和文档

This commit is contained in:
cita
2026-03-03 17:25:27 +08:00
parent cf6b2285fb
commit 9ff138f0b5
13 changed files with 295 additions and 4 deletions
@@ -132,6 +132,38 @@ describe('settings and auth events', () => {
expect(body.message).toContain('Webhook URL');
});
it('rejects telegram config when bot token is missing but telegram is enabled', async () => {
const response = await app.inject({
method: 'PUT',
url: '/api/settings/runtime',
payload: {
telegramEnabled: true,
telegramChatId: '-1001234567890',
telegramBotToken: '',
},
});
expect(response.statusCode).toBe(400);
const body = response.json() as { message?: string };
expect(body.message).toContain('Telegram Bot Token');
});
it('rejects telegram config when chat id is missing but telegram is enabled', async () => {
const response = await app.inject({
method: 'PUT',
url: '/api/settings/runtime',
payload: {
telegramEnabled: true,
telegramBotToken: '123456:telegram-token',
telegramChatId: '',
},
});
expect(response.statusCode).toBe(400);
const body = response.json() as { message?: string };
expect(body.message).toContain('Telegram Chat ID');
});
it('persists and returns routing fallback unit cost from runtime settings', async () => {
const updateResponse = await app.inject({
method: 'PUT',
+68
View File
@@ -21,6 +21,9 @@ interface RuntimeSettingsBody {
barkEnabled?: boolean;
serverChanEnabled?: boolean;
serverChanKey?: string;
telegramEnabled?: boolean;
telegramBotToken?: string;
telegramChatId?: string;
smtpEnabled?: boolean;
smtpHost?: string;
smtpPort?: number;
@@ -155,6 +158,20 @@ function applyImportedSettingToRuntime(key: string, value: unknown) {
config.serverChanKey = value.trim();
return;
}
case 'telegram_enabled': {
config.telegramEnabled = !!value;
return;
}
case 'telegram_bot_token': {
if (typeof value !== 'string') return;
config.telegramBotToken = value.trim();
return;
}
case 'telegram_chat_id': {
if (typeof value !== 'string') return;
config.telegramChatId = value.trim();
return;
}
case 'smtp_enabled': {
config.smtpEnabled = !!value;
return;
@@ -239,6 +256,9 @@ function getRuntimeSettingsResponse(currentAdminIp = '') {
barkEnabled: config.barkEnabled,
serverChanEnabled: config.serverChanEnabled,
serverChanKeyMasked: maskSecret(config.serverChanKey),
telegramEnabled: config.telegramEnabled,
telegramBotTokenMasked: maskSecret(config.telegramBotToken),
telegramChatId: config.telegramChatId,
smtpEnabled: config.smtpEnabled,
smtpHost: config.smtpHost,
smtpPort: config.smtpPort,
@@ -297,6 +317,30 @@ export async function settingsRoutes(app: FastifyInstance) {
}
}
const telegramTouched = body.telegramEnabled !== undefined
|| body.telegramBotToken !== undefined
|| body.telegramChatId !== undefined;
const nextTelegramEnabled = body.telegramEnabled !== undefined
? !!body.telegramEnabled
: config.telegramEnabled;
const nextTelegramBotToken = body.telegramBotToken !== undefined
? String(body.telegramBotToken || '').trim()
: config.telegramBotToken;
const nextTelegramChatId = body.telegramChatId !== undefined
? String(body.telegramChatId || '').trim()
: config.telegramChatId;
if (telegramTouched && nextTelegramEnabled) {
if (!nextTelegramBotToken) {
return reply.code(400).send({ success: false, message: 'Telegram Bot Token 不能为空(启用 Telegram 时)' });
}
if (!nextTelegramBotToken.includes(':')) {
return reply.code(400).send({ success: false, message: 'Telegram Bot Token 格式无效(示例:123456:abcDEF)' });
}
if (!nextTelegramChatId) {
return reply.code(400).send({ success: false, message: 'Telegram Chat ID 不能为空(启用 Telegram 时)' });
}
}
if (body.checkinCron !== undefined) {
if (!cron.validate(body.checkinCron)) {
return reply.code(400).send({ success: false, message: '签到 Cron 表达式无效' });
@@ -382,6 +426,30 @@ export async function settingsRoutes(app: FastifyInstance) {
upsertSetting('serverchan_key', config.serverChanKey);
}
if (body.telegramEnabled !== undefined) {
if (!!body.telegramEnabled !== config.telegramEnabled) {
changedLabels.push('Telegram 开关');
}
config.telegramEnabled = !!body.telegramEnabled;
upsertSetting('telegram_enabled', config.telegramEnabled);
}
if (body.telegramBotToken !== undefined) {
if (String(body.telegramBotToken || '').trim() !== config.telegramBotToken) {
changedLabels.push('Telegram Bot Token');
}
config.telegramBotToken = String(body.telegramBotToken || '').trim();
upsertSetting('telegram_bot_token', config.telegramBotToken);
}
if (body.telegramChatId !== undefined) {
if (String(body.telegramChatId || '').trim() !== config.telegramChatId) {
changedLabels.push('Telegram Chat ID');
}
config.telegramChatId = String(body.telegramChatId || '').trim();
upsertSetting('telegram_chat_id', config.telegramChatId);
}
if (body.smtpEnabled !== undefined) {
if (!!body.smtpEnabled !== config.smtpEnabled) {
changedLabels.push('SMTP 开关');
@@ -187,4 +187,16 @@ describe('resolveUpstreamEndpointCandidates', () => {
expect(isEndpointDowngradeError(405, '{"error":{"message":"Method Not Allowed"}}')).toBe(true);
expect(isEndpointDowngradeError(400, '{"error":{"message":"unsupported endpoint","type":"invalid_request_error"}}')).toBe(true);
});
it('treats Claude Code CLI-only restriction on responses as downgrade candidate', () => {
const upstreamError = JSON.stringify({
error: {
code: 'invalid_request',
type: 'new_api_error',
message: '请勿在 Claude Code CLI 之外使用接口 (request id: abc123)',
},
});
expect(isEndpointDowngradeError(400, upstreamError)).toBe(true);
});
});