claude-code-remote-remake/src/core/notifier.js

234 lines
7.7 KiB
JavaScript

/**
* 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<Object>} 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<Object>} 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;