2025-07-27 02:22:12 +08:00
|
|
|
/**
|
2025-07-28 02:21:38 +08:00
|
|
|
* Claude-Code-Remote Core Notifier
|
2025-07-27 02:22:12 +08:00
|
|
|
* 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();
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-27 17:17:15 +08:00
|
|
|
const langData = this.i18n[lang] || this.i18n['en'];
|
2025-07-27 02:22:12 +08:00
|
|
|
return langData[type] || langData.completed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Load internationalization data
|
|
|
|
|
*/
|
|
|
|
|
_loadI18n() {
|
|
|
|
|
this.i18n = {
|
|
|
|
|
'zh-CN': {
|
|
|
|
|
completed: {
|
2025-07-27 17:17:15 +08:00
|
|
|
title: 'Claude Code - Task Completed',
|
|
|
|
|
message: '[{project}] Task completed, Claude is waiting for next instruction'
|
2025-07-27 02:22:12 +08:00
|
|
|
},
|
|
|
|
|
waiting: {
|
2025-07-27 17:17:15 +08:00
|
|
|
title: 'Claude Code - Waiting for Input',
|
|
|
|
|
message: '[{project}] Claude needs your further guidance'
|
2025-07-27 02:22:12 +08:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
'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;
|