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

234 lines
7.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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