/** * Claude-Code-Remote Core Notifier * Central notification orchestrator that manages multiple channels */ const Logger = require('./logger'); const ConfigManager = require('./config'); class Notifier { constructor(configManager = null) { this.logger = new Logger('Notifier'); this.config = configManager || new ConfigManager(); this.channels = new Map(); this.i18n = null; this._loadI18n(); } /** * Register a notification channel * @param {string} name - Channel name * @param {NotificationChannel} channel - Channel instance */ registerChannel(name, channel) { this.logger.debug(`Registering channel: ${name}`); this.channels.set(name, channel); } /** * Initialize default channels */ async initializeChannels() { this.logger.debug('Initializing channels...'); // Load desktop channel const DesktopChannel = require('../channels/local/desktop'); const desktopConfig = this.config.getChannel('desktop'); if (desktopConfig && desktopConfig.enabled) { const desktop = new DesktopChannel(desktopConfig.config || {}); desktop.config.completedSound = this.config.get('sound.completed'); desktop.config.waitingSound = this.config.get('sound.waiting'); this.registerChannel('desktop', desktop); } // Load email channel const EmailChannel = require('../channels/email/smtp'); const emailConfig = this.config.getChannel('email'); if (emailConfig && emailConfig.enabled) { const email = new EmailChannel(emailConfig.config || {}); this.registerChannel('email', email); } // TODO: Load other channels based on configuration // Discord, Telegram, etc. this.logger.info(`Initialized ${this.channels.size} channels`); } /** * Send notification to all enabled channels * @param {string} type - Notification type: 'completed' | 'waiting' * @param {Object} metadata - Additional metadata * @returns {Promise} Results from all channels */ async notify(type, metadata = {}) { if (!this.config.get('enabled', true)) { this.logger.debug('Notifications disabled'); return { success: false, reason: 'disabled' }; } const notification = this._buildNotification(type, metadata); this.logger.info(`Sending ${type} notification for project: ${notification.project}`); const results = {}; const promises = []; // Send to all channels in parallel for (const [name, channel] of this.channels) { if (channel.enabled) { promises.push( channel.send(notification) .then(success => ({ name, success })) .catch(error => ({ name, success: false, error: error.message })) ); } else { results[name] = { success: false, reason: 'disabled' }; } } // Wait for all channels to complete const channelResults = await Promise.all(promises); channelResults.forEach(result => { results[result.name] = result; }); const successCount = Object.values(results).filter(r => r.success).length; this.logger.info(`Notification sent to ${successCount}/${this.channels.size} channels`); return { success: successCount > 0, results, notification }; } /** * Build notification object from type and metadata * @param {string} type - Notification type * @param {Object} metadata - Additional metadata * @returns {Object} Notification object */ _buildNotification(type, metadata = {}) { const project = metadata.project || this.config.getProjectName(); const lang = this.config.get('language', 'zh-CN'); const content = this._getNotificationContent(type, lang); // Replace project placeholder const message = content.message.replace('{project}', project); // Use custom message if configured const customMessage = this.config.get(`customMessages.${type}`); const finalMessage = customMessage ? customMessage.replace('{project}', project) : message; return { type, title: content.title, message: finalMessage, project, metadata: { timestamp: new Date().toISOString(), language: lang, ...metadata } }; } /** * Get notification content for type and language * @param {string} type - Notification type * @param {string} lang - Language code * @returns {Object} Content object with title and message */ _getNotificationContent(type, lang) { if (!this.i18n) { this._loadI18n(); } const langData = this.i18n[lang] || this.i18n['en']; return langData[type] || langData.completed; } /** * Load internationalization data */ _loadI18n() { this.i18n = { 'zh-CN': { completed: { title: 'Claude Code - Task Completed', message: '[{project}] Task completed, Claude is waiting for next instruction' }, waiting: { title: 'Claude Code - Waiting for Input', message: '[{project}] Claude needs your further guidance' } }, 'en': { completed: { title: 'Claude Code - Task Completed', message: '[{project}] Task completed, Claude is waiting for next instruction' }, waiting: { title: 'Claude Code - Waiting for Input', message: '[{project}] Claude needs your further guidance' } }, 'ja': { completed: { title: 'Claude Code - タスク完了', message: '[{project}] タスクが完了しました。Claudeが次の指示を待っています' }, waiting: { title: 'Claude Code - 入力待ち', message: '[{project}] Claudeにはあなたのさらなるガイダンスが必要です' } } }; } /** * Test all channels * @returns {Promise} Test results */ async test() { this.logger.info('Testing all channels...'); const results = {}; for (const [name, channel] of this.channels) { try { const success = await channel.test(); results[name] = { success }; this.logger.info(`Channel ${name}: ${success ? 'PASS' : 'FAIL'}`); } catch (error) { results[name] = { success: false, error: error.message }; this.logger.error(`Channel ${name}: ERROR - ${error.message}`); } } return results; } /** * Get status of all channels * @returns {Object} Status information */ getStatus() { const channels = {}; for (const [name, channel] of this.channels) { channels[name] = channel.getStatus(); } return { enabled: this.config.get('enabled', true), channels, config: { language: this.config.get('language'), sound: this.config.get('sound'), customMessages: this.config.get('customMessages') } }; } } module.exports = Notifier;