v0.3.5: restore full push notification system (14 channels)

Restored from v0.2.4:
- notifiers/: 14 push channels (bark/serverchan/telegram/lark/webhook/wechat/discord/smtp/...)
- push-user.service.ts: multi-user push config linked to cloud_configs.promotion_account
- notification.service.ts: full dispatcher with per-config + global fallback

New integrations:
- cloud.service.ts: notifyConfigEvent on save_success/cookie_expire/save_fail
- admin.routes.ts: 7 new API endpoints for push users, notify providers, channel test
- database.ts: migration for cloud_configs.notify_config column

How it works:
- Configure push channels in /admin/system-configs (global_notify_config JSON)
- Or per-cloud: link push_users.account = cloud_configs.promotion_account
- Notify events: save_success (green), cookie_expire (red), save_fail >=3 consecutive (yellow)
This commit is contained in:
2026-05-17 05:15:26 +08:00
parent 29e0fcbd43
commit 64b00661a2
22 changed files with 985 additions and 68 deletions

View File

@@ -5,6 +5,7 @@ import { QuarkDriver } from './drivers/quark.driver';
import { BaiduDriver } from './drivers/baidu.driver';
import { CloudConfig, getAndValidateCredential, getActiveCloudConfigs } from './credential.service';
import { lookupIpLocation } from './ip-lookup';
import { notifyConfigEvent } from './notification.service';
/** In-flight save dedup: prevents concurrent saves of the same URL (race condition fix) */
const inFlightSaves = new Map<string, Promise<SaveResult>>();
@@ -174,12 +175,43 @@ async function doSaveFromShare(shareUrl: string, cloudType: string, sourceTitle?
db.prepare(
`UPDATE cloud_configs SET last_used_at = datetime('now','localtime'), total_saves = total_saves + 1, consecutive_failures = 0 WHERE id = ?`
).run(config.id);
const nickname = config.nickname || cloudType;
notifyConfigEvent(config.id, 'save_success', `✅ 转存成功`, `**${cloudType}** · ${nickname}
文件: ${driverResult.folderName || sourceTitle || shareUrl}
耗时: ${((Date.now() - startTime) / 1000).toFixed(1)}s`, 'info', {
file_name: driverResult.folderName || sourceTitle || shareUrl || '',
file_size: '',
cloud_type: cloudType,
nickname: nickname || '',
duration: ((Date.now() - startTime) / 1000).toFixed(1),
share_url: shareUrl,
});
} else if ((driverResult as any).cookieExpired) {
// Cookie expired — don't count as failure, user needs to re-login
notifyConfigEvent(config.id, 'cookie_expire', `⚠️ Cookie过期`, `**${cloudType}** · ${config.nickname || '未知'}
链接: ${shareUrl}
请重新登录`, 'error', {
cloud_type: cloudType,
nickname: config.nickname || '',
share_url: shareUrl,
});
} else {
db.prepare(
`UPDATE cloud_configs SET consecutive_failures = consecutive_failures + 1 WHERE id = ?`
).run(config.id);
const failCount = (db.prepare(`SELECT consecutive_failures FROM cloud_configs WHERE id = ?`).get(config.id) as any)?.consecutive_failures || 0;
if (failCount >= 3) {
notifyConfigEvent(config.id, 'save_fail', `❌ 转存连续失败 ${failCount}`, `**${cloudType}** · ${config.nickname || '未知'}
链接: ${shareUrl}
错误: ${driverResult.message}`, 'warn', {
file_name: sourceTitle || shareUrl || '',
fail_count: String(failCount),
cloud_type: cloudType,
nickname: config.nickname || '',
error: driverResult.message || '',
share_url: shareUrl,
});
}
}
db.prepare(