Files
SimpleRemoter/docs/NotifyFeature_Design.md
2026-04-19 22:55:21 +02:00

432 lines
14 KiB
Markdown
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.
# 通知功能设计方案
## 概述
在主对话框菜单"菜单(&F)"下增加"通知(&N)"功能,支持配置 SMTP 邮件服务,当主机上线且匹配指定条件时自动发送邮件通知。
## 系统要求
- **PowerShell 5.1+**: 内置于 Windows 10 / Server 2016+
- Windows 7/8.1 需手动安装 WMF 5.1
- 不支持 PowerShell 的系统将禁用此功能(对话框控件灰显)
## 核心特性
1. **SMTP 配置**: 支持 Gmail 等主流邮件服务(需应用专用密码)
2. **触发规则**: 主机上线时匹配指定列的文本
3. **备注优先**: 匹配"计算机名"列时,优先使用备注(如有)
4. **多关键词匹配**: 分号分隔,任一匹配即触发(大小写不敏感)
5. **频率控制**: 同一主机 60 分钟内只通知一次
6. **线程安全**: 使用 mutex 保护配置访问
7. **异步发送**: 邮件发送不阻塞主线程
8. **多语言支持**: 支持简体中文、英文、繁体中文
---
## 数据结构
### 常量定义
```cpp
#define NOTIFY_COOLDOWN_MINUTES 15 // 同一主机通知冷却时间(分钟)
```
### 通知类型枚举
```cpp
enum NotifyTriggerType {
NOTIFY_TRIGGER_NONE = 0,
NOTIFY_TRIGGER_HOST_ONLINE = 1, // 主机上线
// 未来可扩展:
// NOTIFY_TRIGGER_HOST_OFFLINE = 2,
// NOTIFY_TRIGGER_FILE_TRANSFER = 3,
};
```
### 单条通知规则
```cpp
struct NotifyRule {
bool enabled; // 是否启用
NotifyTriggerType triggerType; // 触发类型
int columnIndex; // 列编号 (0-based)
std::string matchPattern; // 匹配字符串,分号分隔
};
```
### SMTP 配置
```cpp
struct SmtpConfig {
std::string server; // smtp.gmail.com
int port; // 587
bool useSSL; // true
std::string username; // 发件人邮箱
std::string password; // 应用专用密码 (XOR 加密存储)
std::string recipient; // 收件人 (可选,为空则发给自己)
// 获取实际收件人
std::string GetRecipient() const {
return recipient.empty() ? username : recipient;
}
};
```
### 完整通知配置
```cpp
struct NotifyConfig {
SmtpConfig smtp;
std::vector<NotifyRule> rules; // 规则列表,支持多条
// 频率控制:记录每个主机最后通知时间 (仅内存,不持久化)
std::unordered_map<uint64_t, time_t> lastNotifyTime;
};
```
---
## 配置对话框 UI
```
┌─ 通知设置 ──────────────────────────────────────────────────┐
│ │
│ ⚠️ Warning: Requires Windows 10 or later with PowerShell │
│ 5.1+ (仅不支持时显示) │
│ │
│ ┌─ SMTP 配置 ────────────────────────────────────────────┐ │
│ │ 服务器: [smtp.gmail.com ] 端口: [587 ] │ │
│ │ ☑ 使用 SSL/TLS │ │
│ │ 用户名: [your@gmail.com ] │ │
│ │ 密码: [**************** ] │ │
│ │ (Gmail 需使用应用专用密码) │ │
│ │ 收件人: [ ] [测试] │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ 通知规则 ────────────────────────────────────────────┐ │
│ │ ☑ 启用通知 │ │
│ │ 触发条件: [主机上线 ▼] │ │
│ │ 匹配列: [3 - 计算机名 ▼] │ │
│ │ 关键词: [CEO;CFO;财务;服务器 ] │ │
│ │ (多个关键词用分号分隔,匹配任一项即触发通知) │ │
│ │ 提示: 同一主机 60 分钟内仅通知一次 │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ [确定] [取消] │
└──────────────────────────────────────────────────────────────┘
```
**备注说明**: 收件人为空时,邮件发送给发件人自己(自我通知)。
---
## 邮件内容格式
邮件使用 HTML 格式发送 (`-BodyAsHtml`)。
### 主题
```
[SimpleRemoter] Host Online: WIN-PC01 matched "服务器"
```
### 正文 (HTML)
```html
<b>Host Online Notification</b><br><br>
Trigger Time: 2026-03-11 15:30:45<br>
Match Rule: ComputerName contains "服务器"<br><br>
<b>Host Information:</b><br>
&nbsp;&nbsp;IP Address: 192.168.1.100<br>
&nbsp;&nbsp;Location: 中国-北京<br>
&nbsp;&nbsp;Computer Name: WIN-SERVER-01<br>
&nbsp;&nbsp;OS: Windows Server 2022<br>
&nbsp;&nbsp;Version: 1.2.7<br>
<br>--<br><i>This email was sent automatically by SimpleRemoter</i>
```
---
## 配置持久化
**存储位置**: `%APPDATA%\YAMA\notify.ini`
```ini
[SMTP]
Server=smtp.gmail.com
Port=587
UseSSL=1
Username=your@gmail.com
Password=<XOR加密后的字符串>
Recipient=
[Rule_0]
Enabled=1
TriggerType=1
ColumnIndex=3
MatchPattern=CEO;CFO;服务器
; 未来扩展多规则
;[Rule_1]
;Enabled=1
;...
```
**注意**: `lastNotifyTime` 仅保存在内存中,服务端重启后清空。
---
## 核心逻辑
### PowerShell 检测
```cpp
bool DetectPowerShellSupport() {
std::string cmd = "powershell -NoProfile -Command "
"\"Get-Command Send-MailMessage -ErrorAction SilentlyContinue\"";
DWORD exitCode = 1;
ExecutePowerShell(cmd, &exitCode, true); // hidden
return (exitCode == 0);
}
```
### 频率控制 + 匹配检查 (线程安全)
```cpp
bool NotifyManager::ShouldNotify(context* ctx, std::string& outMatchedKeyword,
const CString& remark)
{
if (!m_powerShellAvailable) return false;
std::lock_guard<std::mutex> lock(m_mutex);
const NotifyRule& rule = m_config.GetRule();
if (!rule.enabled) return false;
if (rule.matchPattern.empty()) return false;
if (!m_config.smtp.IsValid()) return false;
uint64_t clientId = ctx->GetClientID();
time_t now = time(nullptr);
// 冷却检查
auto it = m_config.lastNotifyTime.find(clientId);
if (it != m_config.lastNotifyTime.end()) {
if (now - it->second < NOTIFY_COOLDOWN_MINUTES * 60) {
return false;
}
}
// 获取匹配文本 (计算机名列优先使用备注)
CString colText;
if (rule.columnIndex == ONLINELIST_COMPUTER_NAME && !remark.IsEmpty()) {
colText = remark;
} else {
colText = ctx->GetClientData(rule.columnIndex);
}
if (colText.IsEmpty()) return false;
// 大小写不敏感匹配
std::string colLower = ToLower(CT2A(colText, CP_UTF8));
for (const auto& kw : SplitString(rule.matchPattern, ';')) {
std::string kwLower = ToLower(Trim(kw));
if (!kwLower.empty() && colLower.find(kwLower) != std::string::npos) {
outMatchedKeyword = Trim(kw);
m_config.lastNotifyTime[clientId] = now;
return true;
}
}
return false;
}
```
### 异步邮件发送
```cpp
void NotifyManager::SendNotifyEmailAsync(const std::string& subject,
const std::string& body)
{
if (!m_powerShellAvailable) return;
SmtpConfig smtp;
{
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_config.smtp.IsValid()) return;
smtp = m_config.smtp;
}
std::thread([this, smtp, subject, body]() {
std::ostringstream ps;
ps << "powershell -NoProfile -ExecutionPolicy Bypass -Command \"";
ps << "$pass = ConvertTo-SecureString '" << EscapePowerShell(smtp.password)
<< "' -AsPlainText -Force; ";
ps << "$cred = New-Object PSCredential('" << smtp.username << "', $pass); ";
ps << "Send-MailMessage ";
ps << "-From '" << smtp.username << "' ";
ps << "-To '" << smtp.GetRecipient() << "' ";
ps << "-Subject '" << EscapePowerShell(subject) << "' ";
ps << "-Body '" << EscapePowerShell(body) << "' ";
ps << "-SmtpServer '" << smtp.server << "' ";
ps << "-Port " << smtp.port << " ";
if (smtp.useSSL) ps << "-UseSsl ";
ps << "-Credential $cred -Encoding UTF8 -BodyAsHtml\"";
DWORD exitCode;
ExecutePowerShell(ps.str(), &exitCode, true); // hidden
}).detach();
}
```
---
## 集成点
`2015RemoteDlg.cpp``AddList()` 函数中:
```cpp
// 现有代码
m_ClientIndex[id] = m_HostList.size();
m_HostList.push_back(ContextObject);
// ========== 新增:通知检查 (带异常保护) ==========
try {
std::string matchedKeyword;
CString remark = m_ClientMap->GetClientMapData(ContextObject->GetClientID(), MAP_NOTE);
if (GetNotifyManager().ShouldNotify(ContextObject, matchedKeyword, remark)) {
std::string subject, body;
GetNotifyManager().BuildHostOnlineEmail(ContextObject, matchedKeyword, subject, body);
GetNotifyManager().SendNotifyEmailAsync(subject, body);
}
} catch (...) {
TRACE("[Notify] Exception in notification check\n");
}
// =================================================
ShowMessage(_TR("操作成功"), ...);
```
初始化在 `OnInitDialog()` 中:
```cpp
m_ClientMap->LoadFromFile(GetDbPath());
// 初始化通知管理器
GetNotifyManager().Initialize();
```
---
## 文件改动清单
| 文件 | 改动类型 | 说明 |
|------|---------|------|
| `resource.h` | 修改 | 新增菜单ID、对话框ID、控件ID (33028-33029, 332, 2408-2432) |
| `2015Remote.rc` | 修改 | 添加菜单项、对话框模板、右键菜单"上线提醒" |
| `NotifyConfig.h` | **新增** | 配置结构体定义 |
| `NotifySettingsDlg.h` | **新增** | 配置对话框类声明 (继承 CDialogLangEx) |
| `NotifySettingsDlg.cpp` | **新增** | 配置对话框实现,显式设置控件文本支持翻译 |
| `NotifyManager.h` | **新增** | 通知管理器声明 (单例,线程安全) |
| `NotifyManager.cpp` | **新增** | 检测、发送、配置读写逻辑 |
| `2015RemoteDlg.h` | 修改 | 添加 `OnMenuNotifySettings()`, `OnOnlineLoginNotify()` 方法 |
| `2015RemoteDlg.cpp` | 修改 | 菜单处理 + 上线时调用检查 + 右键快捷添加主机 |
| `lang/en_US.ini` | 修改 | 添加英文翻译 |
| `lang/zh_TW.ini` | 修改 | 添加繁体中文翻译 |
**实际代码量**: 约 1400+ 行
---
## 多语言支持
### 语言文件编码
**重要**: 语言文件使用 **GB2312** 编码,不是 UTF-8
- 位置: `server/2015Remote/lang/*.ini`
- 格式: `简体中文=翻译文本`
### 源文件编码
含中文字符的 C++ 源文件必须使用 **UTF-8 with BOM**,否则 MSVC 会报错 `C2001: 常量中有换行符`
### 翻译示例
```ini
; en_US.ini
通知设置=Notify Settings
SMTP 配置=SMTP Configuration
测试邮件发送成功!=Test email sent successfully!
测试邮件发送失败请检查SMTP配置=Test email failed. Please check SMTP settings.
; zh_TW.ini
通知设置=通知設定
SMTP 配置=SMTP 配置
测试邮件发送成功!=測試郵件發送成功!
```
---
## 测试邮件
- **成功**: 显示 "测试邮件发送成功!"
- **失败**: 显示 "测试邮件发送失败请检查SMTP配置"
- 详细错误信息使用 `TRACE()` 记录到调试日志
---
## 安全考虑
| 风险 | 处理方式 |
|------|---------|
| 密码明文存储 | XOR 混淆后存储 (非安全加密,仅防止直接可见) |
| PowerShell 注入 | 转义单引号 `'``''`,转义换行 |
| 邮件发送失败 | 静默失败,异步执行不影响主流程 |
| 并发访问 | 使用 `std::mutex` 保护配置读写 |
| 通知异常 | try-catch 包裹,异常不影响主机上线 |
---
## 列编号与匹配说明
| 列编号 | 列名称 | 匹配说明 |
|--------|--------|----------|
| 0 | IP地址 | 匹配原始 IP |
| 1 | 地址 | 匹配端口号 |
| 2 | 地理位置 | 匹配位置信息 |
| 3 | 计算机名 | **优先匹配备注**,无备注时匹配计算机名 |
| 4 | 操作系统 | 匹配 OS 信息 |
| 5 | CPU | 匹配 CPU 信息 |
| 6 | 摄像头 | 匹配有/无 |
| 7 | 延迟 | 匹配 RTT 值 |
| 8 | 版本 | 匹配客户端版本 |
| 9 | 安装时间 | 匹配安装时间 |
| 10 | 活动窗口 | 匹配当前窗口标题 |
| 11 | 客户端类型 | 匹配类型标识 |
---
## 使用示例
1. 打开菜单 "菜单(&F)" → "通知(&N)"
2. 配置 SMTP:
- 服务器: `smtp.gmail.com`
- 端口: `587`
- 勾选 SSL/TLS
- 用户名: 你的 Gmail 地址
- 密码: [Gmail 应用专用密码](https://myaccount.google.com/apppasswords)
3. 点击"测试"验证配置
4. 配置规则:
- 勾选"启用通知"
- 选择"计算机名"列
- 输入关键词: `CEO;CFO;财务部`
5. 点击确定保存
当有主机上线且其备注或计算机名包含任一关键词时,你将收到邮件通知。
### 快捷添加主机
右键点击在线主机列表中的主机,选择"上线提醒"可快速将该主机添加到通知关键词列表:
- 自动使用主机备注(如有)或计算机名作为关键词
- 自动去重,已存在的主机不会重复添加
- 添加后自动启用通知规则