This commit is contained in:
Baobhan Sith
2025-04-26 09:36:16 +08:00
parent c141c39bf8
commit 8c2649d9a1
6 changed files with 509 additions and 56 deletions
+192
View File
@@ -2734,6 +2734,95 @@
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/copyfiles": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz",
"integrity": "sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==",
"dev": true,
"license": "MIT",
"dependencies": {
"glob": "^7.0.5",
"minimatch": "^3.0.3",
"mkdirp": "^1.0.4",
"noms": "0.0.0",
"through2": "^2.0.1",
"untildify": "^4.0.0",
"yargs": "^16.1.0"
},
"bin": {
"copyfiles": "copyfiles",
"copyup": "copyfiles"
}
},
"node_modules/copyfiles/node_modules/cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^7.0.0"
}
},
"node_modules/copyfiles/node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/copyfiles/node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/copyfiles/node_modules/yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
"dev": true,
"license": "MIT",
"dependencies": {
"cliui": "^7.0.2",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.0",
"y18n": "^5.0.5",
"yargs-parser": "^20.2.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/copyfiles/node_modules/yargs-parser": {
"version": "20.2.9",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
@@ -3142,6 +3231,16 @@
"@esbuild/win32-x64": "0.21.5"
}
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -4972,6 +5071,44 @@
"node": ">=6.0.0"
}
},
"node_modules/noms": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz",
"integrity": "sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==",
"dev": true,
"license": "ISC",
"dependencies": {
"inherits": "^2.0.1",
"readable-stream": "~1.0.31"
}
},
"node_modules/noms/node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
"dev": true,
"license": "MIT"
},
"node_modules/noms/node_modules/readable-stream": {
"version": "1.0.34",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
"integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==",
"dev": true,
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.1",
"isarray": "0.0.1",
"string_decoder": "~0.10.x"
}
},
"node_modules/noms/node_modules/string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==",
"dev": true,
"license": "MIT"
},
"node_modules/nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
@@ -6445,6 +6582,50 @@
"node": ">=8"
}
},
"node_modules/through2": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
"integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"readable-stream": "~2.3.6",
"xtend": "~4.0.1"
}
},
"node_modules/through2/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dev": true,
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/through2/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true,
"license": "MIT"
},
"node_modules/through2/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/tinyexec": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
@@ -6884,6 +7065,16 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/untildify": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
"integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/untyped": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/untyped/-/untyped-2.0.0.tgz",
@@ -7372,6 +7563,7 @@
"@types/sqlite3": "^3.1.11",
"@types/ssh2": "^1.15.5",
"@types/ws": "^8.18.1",
"copyfiles": "^2.4.1",
"cross-env": "^7.0.3",
"ts-node-dev": "^2.0.0",
"typescript": "^5.0.0"
+2 -1
View File
@@ -4,7 +4,7 @@
"private": true,
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"build": "tsc && copyfiles -u 1 \"src/locales/**/*.json\" dist/src",
"start": "node dist/index.js",
"dev": "cross-env NODE_ENV=development npx ts-node-dev --respawn --transpile-only src/index.ts"
},
@@ -44,6 +44,7 @@
"@types/sqlite3": "^3.1.11",
"@types/ssh2": "^1.15.5",
"@types/ws": "^8.18.1",
"copyfiles": "^2.4.1",
"cross-env": "^7.0.3",
"ts-node-dev": "^2.0.0",
"typescript": "^5.0.0"
+11 -3
View File
@@ -9,10 +9,11 @@ export const defaultLng = 'en';
i18next
.use(Backend)
.init({
// debug: process.env.NODE_ENV === 'development', // 可选:开发模式下开启调试
debug: process.env.NODE_ENV === 'development', // Enable debug logging in dev
supportedLngs,
fallbackLng: defaultLng,
lng: defaultLng, // 默认语言
// lng: defaultLng, // Remove explicit lng setting here, let it be determined later or by detector
preload: supportedLngs, // Preload all supported languages
ns: ['notifications'], // 命名空间,用于组织翻译
defaultNS: 'notifications',
backend: {
@@ -20,8 +21,15 @@ i18next
loadPath: path.join(__dirname, 'locales/{{lng}}/{{ns}}.json'),
},
interpolation: {
escapeValue: false, // 不对插值进行转义,因为我们可能需要 HTML 或 Markdown
escapeValue: false, // Not needed for react apps
},
}, (err, t) => { // Add init callback
if (err) {
return console.error('[i18next] Error during initialization:', err);
}
console.log('[i18next] Initialization complete. Loaded languages:', Object.keys(i18next.store.data));
// console.log('[i18next] Example translation (en):', t('testNotification.subject', { lng: 'en' })); // Optional test
// console.log('[i18next] Example translation (zh):', t('testNotification.subject', { lng: 'zh' })); // Optional test
});
export default i18next;
@@ -0,0 +1,64 @@
{
"testNotification": {
"subject": "Nexus Terminal Test Notification ({eventDisplay})",
"email": {
"body": "This is a test email from Nexus Terminal for event '{{eventDisplay}}'.\n\nIf you received this, your SMTP configuration is working.\n\nTimestamp: {{timestamp}}",
"bodyHtml": "<p>This is a test email from <b>Nexus Terminal</b> for event '{{eventDisplay}}'.</p><p>If you received this, your SMTP configuration is working.</p><p>Timestamp: {{timestamp}}</p>"
},
"webhook": {
"detailsMessage": "This is a test notification from Nexus Terminal (Webhook - i18n) for event '{{eventDisplay}}'."
},
"telegram": {
"detailsMessage": "This is a test notification from Nexus Terminal (Telegram - i18n) for event '{{eventDisplay}}'.",
"bodyTemplate": "*Nexus Terminal Test Notification*\nEvent: `{eventDisplay}`\nTimestamp: {timestamp}\nDetails:\n```\n{details}\n```"
}
},
"eventDisplay": {
"LOGIN_SUCCESS": "Login Success",
"LOGIN_FAILURE": "Login Failure",
"LOGOUT": "Logout",
"PASSWORD_CHANGED": "Password Changed",
"2FA_ENABLED": "2FA Enabled",
"2FA_DISABLED": "2FA Disabled",
"PASSKEY_REGISTERED": "Passkey Registered",
"PASSKEY_DELETED": "Passkey Deleted",
"CONNECTION_CREATED": "Connection Created",
"CONNECTION_UPDATED": "Connection Updated",
"CONNECTION_DELETED": "Connection Deleted",
"CONNECTION_TESTED": "Connection Tested",
"CONNECTIONS_IMPORTED": "Connections Imported",
"CONNECTIONS_EXPORTED": "Connections Exported",
"PROXY_CREATED": "Proxy Created",
"PROXY_UPDATED": "Proxy Updated",
"PROXY_DELETED": "Proxy Deleted",
"TAG_CREATED": "Tag Created",
"TAG_UPDATED": "Tag Updated",
"TAG_DELETED": "Tag Deleted",
"SETTINGS_UPDATED": "Settings Updated",
"IP_WHITELIST_UPDATED": "IP Whitelist Updated",
"NOTIFICATION_SETTING_CREATED": "Notification Setting Created",
"NOTIFICATION_SETTING_UPDATED": "Notification Setting Updated",
"NOTIFICATION_SETTING_DELETED": "Notification Setting Deleted",
"SFTP_ACTION": "SFTP Action",
"SSH_CONNECT_SUCCESS": "SSH Connection Successful",
"SSH_CONNECT_FAILURE": "SSH Connection Failed",
"SSH_SHELL_FAILURE": "SSH Shell Open Failed",
"SERVER_STARTED": "Server Started",
"SERVER_ERROR": "Server Error",
"DATABASE_MIGRATION": "Database Migration",
"ADMIN_SETUP_COMPLETE": "Initial Admin Setup Completed"
},
"eventBody": {
"SETTINGS_UPDATED": "Event: {{eventDisplay}}\nTimestamp: {{timestamp}}\nDetails:\n{{details}}"
},
"connection": {
"testSuccess": "Connection test successful for '{{name}}'!",
"testFailed": "Connection test failed for '{{name}}': {{error}}"
},
"settings": {
"ipWhitelistUpdated": "IP Whitelist updated successfully.",
"updated": "Settings updated successfully."
}
}
@@ -0,0 +1,64 @@
{
"testNotification": {
"subject": "星枢终端测试通知 ({eventDisplay})",
"email": {
"body": "这是一封来自星枢终端关于事件 '{{eventDisplay}}' 的测试邮件。\n\n如果您收到此邮件,表示您的 SMTP 配置工作正常。\n\n时间戳: {{timestamp}}",
"bodyHtml": "<p>这是一封来自 <b>星枢终端</b> 关于事件 '{{eventDisplay}}' 的测试邮件。</p><p>如果您收到此邮件,表示您的 SMTP 配置工作正常。</p><p>时间戳: {{timestamp}}</p>"
},
"webhook": {
"detailsMessage": "这是一条来自星枢终端的测试通知 (Webhook - i18n),事件:'{{eventDisplay}}'。"
},
"telegram": {
"detailsMessage": "这是一条来自星枢终端的测试通知 (Telegram - i18n),事件:'{{eventDisplay}}'。",
"bodyTemplate": "*星枢终端测试通知*\n事件: `{eventDisplay}`\n时间戳: {timestamp}\n详情:\n```\n{details}\n```"
}
},
"eventDisplay": {
"LOGIN_SUCCESS": "登录成功",
"LOGIN_FAILURE": "登录失败",
"LOGOUT": "登出",
"PASSWORD_CHANGED": "密码已更改",
"2FA_ENABLED": "两步验证已启用",
"2FA_DISABLED": "两步验证已禁用",
"PASSKEY_REGISTERED": "通行密钥已注册",
"PASSKEY_DELETED": "通行密钥已删除",
"CONNECTION_CREATED": "连接已创建",
"CONNECTION_UPDATED": "连接已更新",
"CONNECTION_DELETED": "连接已删除",
"CONNECTION_TESTED": "连接已测试",
"CONNECTIONS_IMPORTED": "连接已导入",
"CONNECTIONS_EXPORTED": "连接已导出",
"PROXY_CREATED": "代理已创建",
"PROXY_UPDATED": "代理已更新",
"PROXY_DELETED": "代理已删除",
"TAG_CREATED": "标签已创建",
"TAG_UPDATED": "标签已更新",
"TAG_DELETED": "标签已删除",
"SETTINGS_UPDATED": "设置已更新",
"IP_WHITELIST_UPDATED": "IP 白名单已更新",
"NOTIFICATION_SETTING_CREATED": "通知设置已创建",
"NOTIFICATION_SETTING_UPDATED": "通知设置已更新",
"NOTIFICATION_SETTING_DELETED": "通知设置已删除",
"SFTP_ACTION": "SFTP 操作",
"SSH_CONNECT_SUCCESS": "SSH 连接成功",
"SSH_CONNECT_FAILURE": "SSH 连接失败",
"SSH_SHELL_FAILURE": "SSH Shell 打开失败",
"SERVER_STARTED": "服务器已启动",
"SERVER_ERROR": "服务器错误",
"DATABASE_MIGRATION": "数据库迁移",
"ADMIN_SETUP_COMPLETE": "初始管理员设置完成"
},
"eventBody": {
"SETTINGS_UPDATED": "事件: {{eventDisplay}}\n时间戳: {{timestamp}}\n详情:\n{{details}}"
},
"connection": {
"testSuccess": "连接 '{{name}}' 测试成功!",
"testFailed": "连接 '{{name}}' 测试失败: {{error}}"
},
"settings": {
"ipWhitelistUpdated": "IP 白名单更新成功。",
"updated": "设置更新成功。"
}
}
@@ -14,6 +14,16 @@ import * as nodemailer from 'nodemailer';
import Mail from 'nodemailer/lib/mailer'; // Import Mail type for transporter
import i18next, { defaultLng, supportedLngs } from '../i18n'; // Import i18next instance and config
import { settingsService } from './settings.service'; // Import settings service
// Removed logger import
// Define translation keys for test notifications for clarity
const testSubjectKey = 'testNotification.subject';
const testEmailBodyKey = 'testNotification.email.body';
const testEmailBodyHtmlKey = 'testNotification.email.bodyHtml'; // Separate key for HTML version
const testWebhookDetailsKey = 'testNotification.webhook.detailsMessage';
const testTelegramDetailsKey = 'testNotification.telegram.detailsMessage';
const testTelegramBodyTemplateKey = 'testNotification.telegram.bodyTemplate'; // Key for the template itself
export class NotificationService {
private repository: NotificationSettingsRepository;
@@ -65,10 +75,25 @@ export class NotificationService {
// Specific test method for Email
private async _testEmailSetting(config: EmailConfig): Promise<{ success: boolean; message: string }> {
console.log('[Notification Test - Email] Starting test...'); // Added log
if (!config.to || !config.smtpHost || !config.smtpPort || !config.from) {
console.error('[Notification Test - Email] Missing required config.'); // Added log
return { success: false, message: '测试邮件失败:缺少必要的 SMTP 配置信息 (收件人, 主机, 端口, 发件人)。' };
}
// --- Fetch User Language ---
let userLang = defaultLng;
try {
const langSetting = await settingsService.getSetting('language');
if (langSetting && supportedLngs.includes(langSetting)) {
userLang = langSetting;
}
console.log(`[Notification Test - Email] Using language: ${userLang}`); // Added log
} catch (error) {
console.error(`[Notification Test - Email] Error fetching language setting, using default (${defaultLng}):`, error);
}
// --- End Fetch User Language ---
// Let TypeScript infer the options type for SMTP
const transporterOptions = {
host: config.smtpHost,
@@ -86,46 +111,72 @@ export class NotificationService {
const transporter = nodemailer.createTransport(transporterOptions);
// Translate event display name first
const eventDisplayName = i18next.t(`eventDisplay.SETTINGS_UPDATED`, { lng: userLang, defaultValue: 'SETTINGS_UPDATED' }); // Hardcoding event for test email
const mailOptions: Mail.Options = {
from: config.from,
to: config.to, // Use the 'to' from config for testing
subject: 'Nexus Terminal Test Notification',
text: `This is a test email from Nexus Terminal.\n\nIf you received this, your SMTP configuration is working.\n\nTimestamp: ${new Date().toISOString()}`,
html: `<p>This is a test email from <b>Nexus Terminal</b>.</p><p>If you received this, your SMTP configuration is working.</p><p>Timestamp: ${new Date().toISOString()}</p>`,
// Use i18next for subject and body, using fetched user language
subject: i18next.t(testSubjectKey, { lng: userLang, defaultValue: 'Nexus Terminal Test Notification ({eventDisplay})', eventDisplay: eventDisplayName }),
text: i18next.t(testEmailBodyKey, { lng: userLang, timestamp: new Date().toISOString(), defaultValue: `This is a test email from Nexus Terminal for event '{{eventDisplay}}'.\n\nIf you received this, your SMTP configuration is working.\n\nTimestamp: {{timestamp}}`, eventDisplay: eventDisplayName }),
html: i18next.t(testEmailBodyHtmlKey, { lng: userLang, timestamp: new Date().toISOString(), defaultValue: `<p>This is a test email from <b>Nexus Terminal</b> for event '{{eventDisplay}}'.</p><p>If you received this, your SMTP configuration is working.</p><p>Timestamp: {{timestamp}}</p>`, eventDisplay: eventDisplayName }),
};
try {
console.log(`[Notification Test] Attempting to send test email via ${config.smtpHost}:${config.smtpPort} to ${config.to}`);
console.log(`[Notification Test - Email] Attempting to send test email via ${config.smtpHost}:${config.smtpPort} to ${config.to}`); // Updated log prefix
const info = await transporter.sendMail(mailOptions);
console.log(`[Notification Test] Test email sent successfully: ${info.messageId}`);
console.log(`[Notification Test - Email] Test email sent successfully: ${info.messageId}`); // Updated log prefix
// Verify connection if possible (optional)
// await transporter.verify();
// console.log('[Notification Test] SMTP Connection verified.');
// console.log('[Notification Test - Email] SMTP Connection verified.');
return { success: true, message: '测试邮件发送成功!请检查收件箱。' };
} catch (error: any) {
console.error(`[Notification Test] Error sending test email:`, error);
console.error(`[Notification Test - Email] Error sending test email:`, error); // Updated log prefix
return { success: false, message: `测试邮件发送失败: ${error.message || '未知错误'}` };
}
}
// Specific test method for Webhook
private async _testWebhookSetting(config: WebhookConfig): Promise<{ success: boolean; message: string }> {
console.log('[Notification Test - Webhook] Starting test...'); // Added log
if (!config.url) {
console.error('[Notification Test - Webhook] Missing URL.'); // Added log
return { success: false, message: '测试 Webhook 失败:缺少 URL。' };
}
// --- Fetch User Language ---
let userLang = defaultLng;
try {
const langSetting = await settingsService.getSetting('language');
if (langSetting && supportedLngs.includes(langSetting)) {
userLang = langSetting;
}
console.log(`[Notification Test - Webhook] Using language: ${userLang}`); // Added log
} catch (error) {
console.error(`[Notification Test - Webhook] Error fetching language setting, using default (${defaultLng}):`, error);
}
// --- End Fetch User Language ---
// Use a valid event type for the test payload
const testPayload: NotificationPayload = {
event: 'SETTINGS_UPDATED', // Use a valid event type
timestamp: Date.now(),
details: { message: 'This is a test notification from Nexus Terminal (Webhook).' } // Add channel type
// Use i18next for the details message, using fetched user language
details: { message: i18next.t(testWebhookDetailsKey, { lng: userLang, defaultValue: 'This is a test notification from Nexus Terminal (Webhook).' }) }
};
// Log the translated message safely
const translatedWebhookMessage = (typeof testPayload.details === 'object' && testPayload.details?.message) ? testPayload.details.message : 'Details is not an object with message property';
console.log(`[Notification Test - Webhook] Test payload created. Translated details.message:`, translatedWebhookMessage); // Added log with type check
// Use the same rendering logic as actual sending
// Translate event display name
const eventDisplayName = i18next.t(`eventDisplay.${testPayload.event}`, { lng: userLang, defaultValue: testPayload.event });
// Default body for webhook test, using single braces
const defaultBody = JSON.stringify(testPayload, null, 2);
const defaultBodyTemplate = `Default: JSON payload. Use {event}, {timestamp}, {details}.`; // Default template text
const requestBody = this._renderTemplate(config.bodyTemplate || defaultBodyTemplate, testPayload, defaultBody); // Use default template if user input is empty
const defaultBodyTemplate = `Default: JSON payload. Use {eventDisplay}, {timestamp}, {details}.`; // Updated default template text
// Pass eventDisplayName to renderTemplate
const requestBody = this._renderTemplate(config.bodyTemplate || defaultBodyTemplate, testPayload, defaultBody, eventDisplayName);
const requestConfig: AxiosRequestConfig = {
method: config.method || 'POST',
@@ -139,55 +190,103 @@ export class NotificationService {
};
try {
console.log(`[Notification Test] Sending test Webhook to ${config.url}`);
console.log(`[Notification Test - Webhook] Sending test Webhook to ${config.url}`); // Updated log prefix
const response = await axios(requestConfig);
console.log(`[Notification Test] Test Webhook sent successfully to ${config.url}. Status: ${response.status}`);
console.log(`[Notification Test - Webhook] Test Webhook sent successfully to ${config.url}. Status: ${response.status}`); // Updated log prefix
return { success: true, message: `测试 Webhook 发送成功 (状态码: ${response.status})。` };
} catch (error: any) {
const errorMessage = error.response?.data?.message || error.response?.data || error.message || '未知错误';
console.error(`[Notification Test] Error sending test Webhook to ${config.url}:`, errorMessage);
console.error(`[Notification Test - Webhook] Error sending test Webhook to ${config.url}:`, errorMessage); // Updated log prefix
return { success: false, message: `测试 Webhook 发送失败: ${errorMessage}` };
}
}
// Specific test method for Telegram
private async _testTelegramSetting(config: TelegramConfig): Promise<{ success: boolean; message: string }> {
console.log('[Notification Test - Telegram] Starting test...');
if (!config.botToken || !config.chatId) {
console.error('[Notification Test - Telegram] Missing botToken or chatId.');
return { success: false, message: '测试 Telegram 失败:缺少机器人 Token 或聊天 ID。' };
}
// --- Fetch User Language ---
let userLang = defaultLng;
try {
const langSetting = await settingsService.getSetting('language');
if (langSetting && supportedLngs.includes(langSetting)) {
userLang = langSetting;
}
console.log(`[Notification Test - Telegram] Using language: ${userLang}`); // Added log
} catch (error) {
console.error(`[Notification Test - Telegram] Error fetching language setting, using default (${defaultLng}):`, error);
}
// --- End Fetch User Language ---
// Use a valid event type for the test payload
const testPayload: NotificationPayload = {
event: 'SETTINGS_UPDATED', // Use a valid event type
// Declare payload first, details will be added after translation
const testPayload: NotificationPayload = {
event: 'SETTINGS_UPDATED',
timestamp: Date.now(),
details: { message: 'This is a test notification from Nexus Terminal (Telegram).' } // Add channel type
details: undefined // Initialize details as undefined
};
// --- Translation Start ---
// Log options before calling t() for details message
const detailsOptions = { lng: userLang, defaultValue: 'Fallback: This is a test notification from Nexus Terminal (Telegram).' }; // Use userLang
const keyWithNamespace = `notifications:${testTelegramDetailsKey}`; // Explicitly add namespace
// console.log(`[Notification Test - Telegram] Calling i18next.t for key '${keyWithNamespace}' with options:`, detailsOptions);
const translatedDetailsMessage = i18next.t(keyWithNamespace, detailsOptions); // Use key with namespace
// console.log(`[Notification Test - Telegram] Result from i18next.t for key '${keyWithNamespace}':`, translatedDetailsMessage);
// --- Translation End ---
// Assign the translated details to the existing payload object
testPayload.details = { message: translatedDetailsMessage };
// Log the translated message safely
const messageFromPayload = (typeof testPayload.details === 'object' && testPayload.details?.message) ? testPayload.details.message : 'Details is not an object with message property';
console.log(`[Notification Test - Telegram] Test payload created. Final details.message in payload:`, messageFromPayload); // Updated log description
// Use the same rendering logic as actual sending
// Default message for Telegram test, using single braces and avoiding Markdown issues
const defaultMessageTemplate = `Nexus Terminal Test Notification\nEvent: {event}\nTimestamp: {timestamp}\nDetails:\n{details}`;
const messageText = this._renderTemplate(config.messageTemplate || defaultMessageTemplate, testPayload, ''); // Render template, default is now the template itself
// Get the default template from i18n, fallback to a hardcoded default if key not found
// Also explicitly specify namespace and use userLang for the template key
const templateKeyWithNamespace = `notifications:${testTelegramBodyTemplateKey}`;
const defaultMessageTemplateFromI18n = i18next.t(templateKeyWithNamespace, {
lng: userLang, // Use userLang
defaultValue: `Fallback Template: *Nexus Terminal Test Notification*\nEvent: \`{event}\`\nTimestamp: {timestamp}\nDetails:\n\`\`\`\n{details}\n\`\`\`` // Added Fallback prefix
});
console.log(`[Notification Test - Telegram] Default template from i18n (using lang '${userLang}', key '${templateKeyWithNamespace}'):`, defaultMessageTemplateFromI18n); // Updated log
// Determine which template to use (user's or default from i18n)
const templateToUse = config.messageTemplate || defaultMessageTemplateFromI18n;
console.log(`[Notification Test - Telegram] Template to render:`, templateToUse); // Added log
// Translate event display name
const eventDisplayName = i18next.t(`eventDisplay.${testPayload.event}`, { lng: userLang, defaultValue: testPayload.event });
// Render the template, passing eventDisplayName
const messageText = this._renderTemplate(templateToUse, testPayload, '', eventDisplayName);
console.log(`[Notification Test - Telegram] Rendered message text:`, messageText);
const telegramApiUrl = `https://api.telegram.org/bot${config.botToken}/sendMessage`;
try {
console.log(`[Notification Test] Sending test Telegram message to chat ID ${config.chatId}`);
console.log(`[Notification Test - Telegram] Sending test Telegram message to chat ID ${config.chatId}`); // Updated log prefix
const response = await axios.post(telegramApiUrl, {
chat_id: config.chatId,
text: messageText,
// No parse_mode for testing to avoid issues with braces
parse_mode: 'Markdown' // Add parse_mode for testing consistency
}, { timeout: 15000 }); // Slightly longer timeout for testing
if (response.data?.ok) {
console.log(`[Notification Test] Test Telegram message sent successfully.`);
return { success: true, message: '测试 Telegram 消息发送成功!' };
} else {
console.error(`[Notification Test] Telegram API returned error:`, response.data?.description);
return { success: false, message: `测试 Telegram 发送失败: ${response.data?.description || 'API 返回失败'}` };
}
} catch (error: any) {
const errorMessage = error.response?.data?.description || error.response?.data || error.message || '未知错误';
console.error(`[Notification Test] Error sending test Telegram message:`, errorMessage);
console.log(`[Notification Test - Telegram] Test Telegram message sent successfully.`); // Updated log prefix
return { success: true, message: '测试 Telegram 消息发送成功!' };
} else {
console.error(`[Notification Test - Telegram] Telegram API returned error:`, response.data?.description); // Updated log prefix
return { success: false, message: `测试 Telegram 发送失败: ${response.data?.description || 'API 返回失败'}` };
}
} catch (error: any) {
const errorMessage = error.response?.data?.description || error.response?.data || error.message || '未知错误';
console.error(`[Notification Test - Telegram] Error sending test Telegram message:`, errorMessage); // Updated log prefix
return { success: false, message: `测试 Telegram 发送失败: ${errorMessage}` };
}
}
@@ -253,11 +352,13 @@ export class NotificationService {
// --- Private Sending Helpers ---
private _renderTemplate(template: string | undefined, payload: NotificationPayload, defaultText: string): string {
// Updated to accept eventDisplayName
private _renderTemplate(template: string | undefined, payload: NotificationPayload, defaultText: string, eventDisplayName?: string): string {
if (!template) return defaultText;
let rendered = template;
// Replace single-brace placeholders
rendered = rendered.replace(/\{event\}/g, payload.event);
rendered = rendered.replace(/\{event\}/g, payload.event); // Keep original event code if needed
rendered = rendered.replace(/\{eventDisplay\}/g, eventDisplayName || payload.event); // Use translated name, fallback to original code
rendered = rendered.replace(/\{timestamp\}/g, new Date(payload.timestamp).toISOString());
const detailsString = typeof payload.details === 'string' ? payload.details : JSON.stringify(payload.details || {}, null, 2);
rendered = rendered.replace(/\{details\}/g, detailsString);
@@ -272,16 +373,19 @@ export class NotificationService {
return;
}
// Translate event display name
const eventDisplayName = i18next.t(`eventDisplay.${payload.event}`, { lng: userLang, defaultValue: payload.event });
// Translate payload details if they match a known key structure
const translatedDetails = this._translatePayloadDetails(payload.details, userLang);
const translatedPayload = { ...payload, details: translatedDetails };
const translatedPayload = { ...payload, details: translatedDetails }; // Keep original payload structure for details translation
const defaultBody = JSON.stringify(translatedPayload, null, 2);
const defaultBody = JSON.stringify(translatedPayload, null, 2); // Default body still uses the potentially translated details
// Note: Webhook body templates might need adjustments if they expect specific structures
// or if they should also be translated. For now, we only translate the 'details'.
// Use default template text if user hasn't provided one
const defaultBodyTemplate = `Default: JSON payload. Use {event}, {timestamp}, {details}.`;
const requestBody = this._renderTemplate(config.bodyTemplate || defaultBodyTemplate, translatedPayload, defaultBody);
const defaultBodyTemplate = `Default: JSON payload. Use {eventDisplay}, {timestamp}, {details}.`; // Updated placeholder
// Pass eventDisplayName to renderTemplate
const requestBody = this._renderTemplate(config.bodyTemplate || defaultBodyTemplate, translatedPayload, defaultBody, eventDisplayName);
const requestConfig: AxiosRequestConfig = {
method: config.method || 'POST',
@@ -335,20 +439,30 @@ export class NotificationService {
i18nOptions.details = payload.details; // Pass non-object details directly if needed
}
// Translate event display name first
const eventDisplayName = i18next.t(`eventDisplay.${payload.event}`, { lng: userLang, defaultValue: payload.event });
// Try to translate the event itself for the subject, fallback to event name
const defaultSubjectKey = `event.${payload.event}`;
const defaultSubjectFallback = `星枢终端通知: {event}`; // Use single brace
const subjectText = i18next.t(defaultSubjectKey, { ...i18nOptions, defaultValue: defaultSubjectFallback });
// Use default subject template if user hasn't provided one
const defaultSubjectTemplate = `Notification: {event}`;
const subject = this._renderTemplate(config.subjectTemplate || defaultSubjectTemplate, payload, subjectText);
const defaultSubjectKey = `event.${payload.event}`; // This key might not exist, rely on template or default below
const defaultSubjectFallback = `Nexus Terminal Notification: {eventDisplay}`; // Use eventDisplay in fallback
const subjectText = i18next.t(defaultSubjectKey, { ...i18nOptions, defaultValue: defaultSubjectFallback, eventDisplay: eventDisplayName }); // Pass eventDisplay for interpolation in fallback
// Use default subject template from i18n if user hasn't provided one
const defaultSubjectTemplateKey = 'testNotification.subject'; // Reuse test subject key structure
const defaultSubjectTemplate = i18next.t(defaultSubjectTemplateKey, { lng: userLang, defaultValue: defaultSubjectFallback, eventDisplay: eventDisplayName });
// Render the subject template, passing the translated event display name
const subject = this._renderTemplate(config.subjectTemplate || defaultSubjectTemplate, payload, subjectText, eventDisplayName);
// Translate the main body content based on event type if a key exists
const bodyKey = `eventBody.${payload.event}`;
const detailsString = typeof payload.details === 'string' ? payload.details : JSON.stringify(payload.details || {}, null, 2);
const defaultBodyText = `事件: ${payload.event}\n时间: ${new Date(payload.timestamp).toISOString()}\n详情:\n${detailsString}`;
const body = i18next.t(bodyKey, { ...i18nOptions, defaultValue: defaultBodyText });
// Note: Email body templates are not implemented in this version. Using translated/default text.
// Use eventDisplay in the default body text
const defaultBodyText = `Event: ${eventDisplayName}\nTimestamp: ${new Date(payload.timestamp).toISOString()}\nDetails:\n${detailsString}`;
// Pass eventDisplay for interpolation if the translation key uses it
const body = i18next.t(bodyKey, { ...i18nOptions, defaultValue: defaultBodyText, eventDisplay: eventDisplayName });
// Note: Email body templates are not implemented. Using translated/default text.
// If templates were implemented, we'd use _renderTemplate here too.
const mailOptions: Mail.Options = {
from: config.from,
@@ -383,13 +497,23 @@ export class NotificationService {
} else if (payload.details !== undefined) {
i18nOptions.details = payload.details; // Pass non-object details directly if needed
}
const messageKey = `eventBody.${payload.event}`; // Use same key as email body for consistency
const detailsStr = payload.details ? `\n详情: \`\`\`\n${typeof payload.details === 'string' ? payload.details : JSON.stringify(payload.details, null, 2)}\n\`\`\`` : '';
const defaultMessageTemplate = `*Nexus Terminal Notification*\n\nEvent: \`{event}\`\nTimestamp: {timestamp}${detailsStr}`; // Use single brace
const translatedBody = i18next.t(messageKey, { ...i18nOptions, defaultValue: defaultMessageTemplate });
// Translate event display name first
const eventDisplayName = i18next.t(`eventDisplay.${payload.event}`, { lng: userLang, defaultValue: payload.event });
// Allow template override, use default template if user input is empty
const messageText = this._renderTemplate(config.messageTemplate || defaultMessageTemplate, payload, translatedBody);
const messageKey = `eventBody.${payload.event}`; // Use same key as email body for consistency
const detailsStr = payload.details ? `\nDetails: \`\`\`\n${typeof payload.details === 'string' ? payload.details : JSON.stringify(payload.details, null, 2)}\n\`\`\`` : '';
// Use eventDisplay in the default message template fallback
const defaultMessageTemplateFallback = `*Nexus Terminal Notification*\n\nEvent: \`{eventDisplay}\`\nTimestamp: {timestamp}${detailsStr}`;
// Pass eventDisplay for interpolation if the translation key uses it
const translatedBody = i18next.t(messageKey, { ...i18nOptions, defaultValue: defaultMessageTemplateFallback, eventDisplay: eventDisplayName });
// Get the default template from i18n (using the test key structure)
const defaultTemplateKey = `notifications:${testTelegramBodyTemplateKey}`;
const defaultMessageTemplateFromI18n = i18next.t(defaultTemplateKey, { lng: userLang, defaultValue: translatedBody, eventDisplay: eventDisplayName });
// Allow template override, use default template from i18n if user input is empty
// Pass eventDisplayName to renderTemplate
const messageText = this._renderTemplate(config.messageTemplate || defaultMessageTemplateFromI18n, payload, translatedBody, eventDisplayName);
const telegramApiUrl = `https://api.telegram.org/bot${config.botToken}/sendMessage`;
try {