234 lines
7.7 KiB
JavaScript
234 lines
7.7 KiB
JavaScript
/**
|
||
* TaskPing 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['zh-CN'];
|
||
return langData[type] || langData.completed;
|
||
}
|
||
|
||
/**
|
||
* Load internationalization data
|
||
*/
|
||
_loadI18n() {
|
||
this.i18n = {
|
||
'zh-CN': {
|
||
completed: {
|
||
title: 'Claude Code - 任务完成',
|
||
message: '[{project}] 任务已完成,Claude正在等待下一步指令'
|
||
},
|
||
waiting: {
|
||
title: 'Claude Code - 等待输入',
|
||
message: '[{project}] Claude需要您的进一步指导'
|
||
}
|
||
},
|
||
'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; |