Init: Migrate SimpleRemoter (Since v1.3.1) to Gitea
This commit is contained in:
431
docs/NotifyFeature_Design.md
Normal file
431
docs/NotifyFeature_Design.md
Normal file
@@ -0,0 +1,431 @@
|
||||
# 通知功能设计方案
|
||||
|
||||
## 概述
|
||||
|
||||
在主对话框菜单"菜单(&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>
|
||||
IP Address: 192.168.1.100<br>
|
||||
Location: 中国-北京<br>
|
||||
Computer Name: WIN-SERVER-01<br>
|
||||
OS: Windows Server 2022<br>
|
||||
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. 点击确定保存
|
||||
|
||||
当有主机上线且其备注或计算机名包含任一关键词时,你将收到邮件通知。
|
||||
|
||||
### 快捷添加主机
|
||||
|
||||
右键点击在线主机列表中的主机,选择"上线提醒"可快速将该主机添加到通知关键词列表:
|
||||
|
||||
- 自动使用主机备注(如有)或计算机名作为关键词
|
||||
- 自动去重,已存在的主机不会重复添加
|
||||
- 添加后自动启用通知规则
|
||||
Reference in New Issue
Block a user