Feature: Add "Online Notify" to log message context menu in main dialog
- Add "Online Notify" menu item to the log message list (m_CList_Message) right-click context menu, allowing users to extract IPv4 addresses from selected log entries and add them to notification keywords - Implement ExtractFirstIPFromMessage() for dependency-free IPv4 parsing with per-segment 0-255 validation, handling diverse log message formats - Extend NotifyManager::ShouldNotify with IP column (ONLINELIST_IP) fallback check, ensuring IP keywords match regardless of the user's configured ColumnIndex setting - OnMsglogLoginNotify only appends keywords and enables the rule without overwriting the user's existing ColumnIndex or TriggerType settings - Menu item is automatically grayed out when PowerShell is unavailable or no log entry is selected - Sync en_US / zh_TW language files with new translation strings Co-Authored-By: DeepSeek V4 Pro
This commit is contained in:
@@ -942,6 +942,7 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
|
|||||||
ON_COMMAND(ID_MSGLOG_COPY, &CMy2015RemoteDlg::OnMsglogCopy)
|
ON_COMMAND(ID_MSGLOG_COPY, &CMy2015RemoteDlg::OnMsglogCopy)
|
||||||
ON_COMMAND(ID_MSGLOG_CLEAR, &CMy2015RemoteDlg::OnMsglogClear)
|
ON_COMMAND(ID_MSGLOG_CLEAR, &CMy2015RemoteDlg::OnMsglogClear)
|
||||||
ON_COMMAND(ID_MSGLOG_SEARCH, &CMy2015RemoteDlg::OnMsglogSearch)
|
ON_COMMAND(ID_MSGLOG_SEARCH, &CMy2015RemoteDlg::OnMsglogSearch)
|
||||||
|
ON_COMMAND(ID_MSGLOG_LOGIN_NOTIFY, &CMy2015RemoteDlg::OnMsglogLoginNotify)
|
||||||
ON_COMMAND(ID_ONLINE_ADD_WATCH, &CMy2015RemoteDlg::OnOnlineAddWatch)
|
ON_COMMAND(ID_ONLINE_ADD_WATCH, &CMy2015RemoteDlg::OnOnlineAddWatch)
|
||||||
ON_COMMAND(ID_ONLINE_LOGIN_NOTIFY, &CMy2015RemoteDlg::OnOnlineLoginNotify)
|
ON_COMMAND(ID_ONLINE_LOGIN_NOTIFY, &CMy2015RemoteDlg::OnOnlineLoginNotify)
|
||||||
ON_NOTIFY(NM_CUSTOMDRAW, IDC_ONLINE, &CMy2015RemoteDlg::OnNMCustomdrawOnline)
|
ON_NOTIFY(NM_CUSTOMDRAW, IDC_ONLINE, &CMy2015RemoteDlg::OnNMCustomdrawOnline)
|
||||||
@@ -9303,11 +9304,18 @@ void CMy2015RemoteDlg::OnRClickMessage(NMHDR* pNMHDR, LRESULT* pResult)
|
|||||||
menu.AppendMenu(MF_SEPARATOR);
|
menu.AppendMenu(MF_SEPARATOR);
|
||||||
menu.AppendMenu(MF_STRING, ID_MSGLOG_SEARCH,
|
menu.AppendMenu(MF_STRING, ID_MSGLOG_SEARCH,
|
||||||
m_bLogSearchVisible ? _TR("隐藏搜索") : _TR("搜索日志"));
|
m_bLogSearchVisible ? _TR("隐藏搜索") : _TR("搜索日志"));
|
||||||
|
menu.AppendMenu(MF_SEPARATOR);
|
||||||
|
menu.AppendMenu(MF_STRING, ID_MSGLOG_LOGIN_NOTIFY, _TR("上线提醒"));
|
||||||
|
|
||||||
// 没有选中项时禁用"删除选中"
|
// 没有选中项时禁用"删除选中"和"上线提醒"
|
||||||
if (m_CList_Message.GetSelectedCount() == 0) {
|
if (m_CList_Message.GetSelectedCount() == 0) {
|
||||||
menu.EnableMenuItem(ID_MSGLOG_DELETE, MF_GRAYED);
|
menu.EnableMenuItem(ID_MSGLOG_DELETE, MF_GRAYED);
|
||||||
menu.EnableMenuItem(ID_MSGLOG_COPY, MF_GRAYED);
|
menu.EnableMenuItem(ID_MSGLOG_COPY, MF_GRAYED);
|
||||||
|
menu.EnableMenuItem(ID_MSGLOG_LOGIN_NOTIFY, MF_GRAYED);
|
||||||
|
}
|
||||||
|
// PowerShell不可用时禁用"上线提醒"
|
||||||
|
if (!GetNotifyManager().IsPowerShellAvailable()) {
|
||||||
|
menu.EnableMenuItem(ID_MSGLOG_LOGIN_NOTIFY, MF_GRAYED);
|
||||||
}
|
}
|
||||||
// 列表为空时禁用"清空日志"
|
// 列表为空时禁用"清空日志"
|
||||||
if (m_CList_Message.GetItemCount() == 0) {
|
if (m_CList_Message.GetItemCount() == 0) {
|
||||||
@@ -9379,6 +9387,168 @@ void CMy2015RemoteDlg::OnMsglogSearch()
|
|||||||
OnSize(SIZE_RESTORED, rc.Width(), rc.Height());
|
OnSize(SIZE_RESTORED, rc.Width(), rc.Height());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract the first valid IPv4 address from a log message text.
|
||||||
|
// Returns true and sets outIP if a valid IPv4 address is found.
|
||||||
|
static bool ExtractFirstIPFromMessage(const CString& msg, CString& outIP)
|
||||||
|
{
|
||||||
|
// Scan the string for patterns like d.d.d.d and validate each segment
|
||||||
|
int len = msg.GetLength();
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
TCHAR ch = msg[i];
|
||||||
|
// Look for start of a potential IP: digit
|
||||||
|
if (ch >= _T('0') && ch <= _T('9')) {
|
||||||
|
int segments[4] = { -1, -1, -1, -1 };
|
||||||
|
int segIdx = 0;
|
||||||
|
int numStart = i;
|
||||||
|
|
||||||
|
for (int j = i; j < len && segIdx < 4; j++) {
|
||||||
|
TCHAR c = msg[j];
|
||||||
|
if (c >= _T('0') && c <= _T('9')) {
|
||||||
|
if (numStart < 0) numStart = j;
|
||||||
|
} else if (c == _T('.')) {
|
||||||
|
if (numStart < 0) break; // consecutive dots or leading dot
|
||||||
|
// Parse the number
|
||||||
|
CString numStr = msg.Mid(numStart, j - numStart);
|
||||||
|
if (numStr.GetLength() > 3) break; // too many digits
|
||||||
|
int val = _tstoi(numStr);
|
||||||
|
if (val > 255) break;
|
||||||
|
segments[segIdx] = val;
|
||||||
|
segIdx++;
|
||||||
|
numStart = -1;
|
||||||
|
if (segIdx >= 4) break; // too many segments
|
||||||
|
} else {
|
||||||
|
break; // non-IP character
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have exactly 4 valid segments and the last char ends a number
|
||||||
|
if (segIdx == 3 && numStart >= 0) {
|
||||||
|
// Find where the number ends
|
||||||
|
int numEnd = numStart;
|
||||||
|
while (numEnd < len && msg[numEnd] >= _T('0') && msg[numEnd] <= _T('9'))
|
||||||
|
numEnd++;
|
||||||
|
CString numStr = msg.Mid(numStart, numEnd - numStart);
|
||||||
|
if (numStr.GetLength() <= 3) {
|
||||||
|
int val = _tstoi(numStr);
|
||||||
|
if (val >= 0 && val <= 255) {
|
||||||
|
segments[3] = val;
|
||||||
|
// Success: build the IP string
|
||||||
|
outIP.Format(_T("%d.%d.%d.%d"), segments[0], segments[1], segments[2], segments[3]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Skip past the partial match to continue scanning
|
||||||
|
// (the outer loop will increment i, which is fine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMy2015RemoteDlg::OnMsglogLoginNotify()
|
||||||
|
{
|
||||||
|
// Get selected log messages and extract IP addresses from them
|
||||||
|
std::vector<std::string> ipsToAdd;
|
||||||
|
|
||||||
|
POSITION pos = m_CList_Message.GetFirstSelectedItemPosition();
|
||||||
|
while (pos) {
|
||||||
|
int row = m_CList_Message.GetNextSelectedItem(pos);
|
||||||
|
// Column 2 is the message content ("信息内容")
|
||||||
|
CString msgText = m_CList_Message.GetItemText(row, 2);
|
||||||
|
|
||||||
|
CString extractedIP;
|
||||||
|
if (ExtractFirstIPFromMessage(msgText, extractedIP)) {
|
||||||
|
std::string ipUtf8 = CT2A(extractedIP, CP_UTF8);
|
||||||
|
ipsToAdd.push_back(ipUtf8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ipsToAdd.empty()) {
|
||||||
|
MessageBoxL("所选日志消息中未检测到有效的 IP 地址", "提示", MB_ICONINFORMATION);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to convert string to lowercase
|
||||||
|
auto toLower = [](const std::string& str) -> std::string {
|
||||||
|
std::string result = str;
|
||||||
|
std::transform(result.begin(), result.end(), result.begin(), ::tolower);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use the same deduplication logic as OnOnlineLoginNotify
|
||||||
|
NotifyConfig config = GetNotifyManager().GetConfig();
|
||||||
|
NotifyRule& rule = config.GetRule();
|
||||||
|
|
||||||
|
// Parse existing keywords into a set for deduplication (case-insensitive)
|
||||||
|
std::set<std::string> existingKeywordsLower;
|
||||||
|
std::vector<std::string> existingKeywords;
|
||||||
|
std::string pattern = rule.matchPattern;
|
||||||
|
size_t patternPos = 0;
|
||||||
|
while ((patternPos = pattern.find(';')) != std::string::npos) {
|
||||||
|
std::string kw = pattern.substr(0, patternPos);
|
||||||
|
size_t start = kw.find_first_not_of(" \t");
|
||||||
|
size_t end = kw.find_last_not_of(" \t");
|
||||||
|
if (start != std::string::npos) {
|
||||||
|
std::string trimmed = kw.substr(start, end - start + 1);
|
||||||
|
existingKeywordsLower.insert(toLower(trimmed));
|
||||||
|
existingKeywords.push_back(trimmed);
|
||||||
|
}
|
||||||
|
pattern.erase(0, patternPos + 1);
|
||||||
|
}
|
||||||
|
if (!pattern.empty()) {
|
||||||
|
size_t start = pattern.find_first_not_of(" \t");
|
||||||
|
size_t end = pattern.find_last_not_of(" \t");
|
||||||
|
if (start != std::string::npos) {
|
||||||
|
std::string trimmed = pattern.substr(start, end - start + 1);
|
||||||
|
existingKeywordsLower.insert(toLower(trimmed));
|
||||||
|
existingKeywords.push_back(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new IPs if not already present
|
||||||
|
int addedCount = 0;
|
||||||
|
for (const auto& ip : ipsToAdd) {
|
||||||
|
if (existingKeywordsLower.find(toLower(ip)) == existingKeywordsLower.end()) {
|
||||||
|
existingKeywordsLower.insert(toLower(ip));
|
||||||
|
existingKeywords.push_back(ip);
|
||||||
|
addedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addedCount == 0) {
|
||||||
|
MessageBoxL("所有解析出的 IP 已在上线提醒列表中", "提示", MB_ICONINFORMATION);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild match pattern
|
||||||
|
std::string newPattern;
|
||||||
|
for (const auto& kw : existingKeywords) {
|
||||||
|
if (!newPattern.empty()) {
|
||||||
|
newPattern += ";";
|
||||||
|
}
|
||||||
|
newPattern += kw;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update config (preserve existing columnIndex and triggerType)
|
||||||
|
rule.matchPattern = newPattern;
|
||||||
|
rule.enabled = true;
|
||||||
|
if (rule.triggerType == NOTIFY_TRIGGER_NONE) {
|
||||||
|
rule.triggerType = NOTIFY_TRIGGER_HOST_ONLINE;
|
||||||
|
}
|
||||||
|
|
||||||
|
GetNotifyManager().SetConfig(config);
|
||||||
|
GetNotifyManager().SaveConfig();
|
||||||
|
|
||||||
|
// Build message with SMTP warning if not configured
|
||||||
|
CString msg;
|
||||||
|
msg.Format(_TR("已添加 %d 个 IP 到上线提醒列表"), addedCount);
|
||||||
|
if (!config.smtp.IsValid()) {
|
||||||
|
msg += _T("\n\n");
|
||||||
|
msg += _TR("注意: SMTP 未配置,请先在通知设置中配置邮箱");
|
||||||
|
}
|
||||||
|
MessageBoxL(msg, _TR("上线提醒"), MB_ICONINFORMATION);
|
||||||
|
}
|
||||||
|
|
||||||
void CMy2015RemoteDlg::OnOnlineAddWatch()
|
void CMy2015RemoteDlg::OnOnlineAddWatch()
|
||||||
{
|
{
|
||||||
EnterCriticalSection(&m_cs);
|
EnterCriticalSection(&m_cs);
|
||||||
|
|||||||
@@ -564,6 +564,7 @@ public:
|
|||||||
afx_msg void OnMsglogCopy();
|
afx_msg void OnMsglogCopy();
|
||||||
afx_msg void OnMsglogClear();
|
afx_msg void OnMsglogClear();
|
||||||
afx_msg void OnMsglogSearch();
|
afx_msg void OnMsglogSearch();
|
||||||
|
afx_msg void OnMsglogLoginNotify();
|
||||||
afx_msg void OnOnlineAddWatch();
|
afx_msg void OnOnlineAddWatch();
|
||||||
afx_msg void OnNMCustomdrawOnline(NMHDR* pNMHDR, LRESULT* pResult);
|
afx_msg void OnNMCustomdrawOnline(NMHDR* pNMHDR, LRESULT* pResult);
|
||||||
afx_msg void OnOnlineRunAsAdmin();
|
afx_msg void OnOnlineRunAsAdmin();
|
||||||
|
|||||||
@@ -141,35 +141,52 @@ bool NotifyManager::ShouldNotify(context* ctx, std::string& outMatchedKeyword, c
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get column text (for COMPUTER_NAME column, prefer remark if available)
|
// Split pattern by semicolon and get keywords
|
||||||
|
std::vector<std::string> keywords = SplitString(rule.matchPattern, ';');
|
||||||
|
|
||||||
|
// Helper: check if any keyword matches a given column text
|
||||||
|
auto matchColumn = [&keywords, this](const CString& colText) -> std::string {
|
||||||
|
if (colText.IsEmpty()) return "";
|
||||||
|
std::string colTextStr = CT2A(colText, CP_UTF8);
|
||||||
|
std::string colLower = colTextStr;
|
||||||
|
std::transform(colLower.begin(), colLower.end(), colLower.begin(), ::tolower);
|
||||||
|
|
||||||
|
for (const auto& kw : keywords) {
|
||||||
|
std::string trimmed = Trim(kw);
|
||||||
|
if (trimmed.empty()) continue;
|
||||||
|
|
||||||
|
std::string kwLower = trimmed;
|
||||||
|
std::transform(kwLower.begin(), kwLower.end(), kwLower.begin(), ::tolower);
|
||||||
|
|
||||||
|
if (colLower.find(kwLower) != std::string::npos) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check primary column (for COMPUTER_NAME column, prefer remark if available)
|
||||||
CString colText;
|
CString colText;
|
||||||
if (rule.columnIndex == ONLINELIST_COMPUTER_NAME && !remark.IsEmpty()) {
|
if (rule.columnIndex == ONLINELIST_COMPUTER_NAME && !remark.IsEmpty()) {
|
||||||
colText = remark;
|
colText = remark;
|
||||||
} else {
|
} else {
|
||||||
colText = ctx->GetClientData(rule.columnIndex);
|
colText = ctx->GetClientData(rule.columnIndex);
|
||||||
}
|
}
|
||||||
if (colText.IsEmpty()) return false;
|
std::string matched = matchColumn(colText);
|
||||||
|
if (!matched.empty()) {
|
||||||
|
outMatchedKeyword = matched;
|
||||||
|
m_config.lastNotifyTime[clientId] = now;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Convert to std::string for matching
|
// Also check the IP column (ONLINELIST_IP) so IP keywords added from log
|
||||||
std::string colTextStr = CT2A(colText, CP_UTF8);
|
// messages can match against host IP addresses when they come online.
|
||||||
|
CString ipText = ctx->GetClientData(ONLINELIST_IP);
|
||||||
// Split pattern by semicolon and check each keyword
|
matched = matchColumn(ipText);
|
||||||
std::vector<std::string> keywords = SplitString(rule.matchPattern, ';');
|
if (!matched.empty()) {
|
||||||
for (const auto& kw : keywords) {
|
outMatchedKeyword = matched;
|
||||||
std::string trimmed = Trim(kw);
|
m_config.lastNotifyTime[clientId] = now;
|
||||||
if (trimmed.empty()) continue;
|
return true;
|
||||||
|
|
||||||
// Case-insensitive substring search
|
|
||||||
std::string colLower = colTextStr;
|
|
||||||
std::string kwLower = trimmed;
|
|
||||||
std::transform(colLower.begin(), colLower.end(), colLower.begin(), ::tolower);
|
|
||||||
std::transform(kwLower.begin(), kwLower.end(), kwLower.begin(), ::tolower);
|
|
||||||
|
|
||||||
if (colLower.find(kwLower) != std::string::npos) {
|
|
||||||
outMatchedKeyword = trimmed;
|
|
||||||
m_config.lastNotifyTime[clientId] = now;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1965,3 +1965,6 @@ FRPC Զ
|
|||||||
搜索日志=Search Logs
|
搜索日志=Search Logs
|
||||||
隐藏搜索=Hide Search
|
隐藏搜索=Hide Search
|
||||||
请=Please
|
请=Please
|
||||||
|
所选日志消息中未检测到有效的 IP 地址=No valid IP address detected in the selected log messages
|
||||||
|
所有解析出的 IP 已在上线提醒列表中=All extracted IPs are already in the login notify list
|
||||||
|
已添加 %d 个 IP 到上线提醒列表=Added %d IP(s) to login notify list
|
||||||
|
|||||||
@@ -1956,3 +1956,6 @@ FRPC Զ
|
|||||||
搜索日志=搜索日志
|
搜索日志=搜索日志
|
||||||
隐藏搜索=隐藏搜索
|
隐藏搜索=隐藏搜索
|
||||||
请=请
|
请=请
|
||||||
|
所有解析出的 IP 已在上线提醒列表中=所有解析出的 IP 已在上線提醒列表中
|
||||||
|
已添加 %d 个 IP 到上线提醒列表=已新增 %d 個 IP 到上線提醒列表
|
||||||
|
所选日志消息中未检测到有效的 IP 地址=所選日誌消息中未檢測到有效的 IP 位址
|
||||||
|
|||||||
@@ -991,6 +991,7 @@
|
|||||||
#define ID_CANCEL_SHARE 33042
|
#define ID_CANCEL_SHARE 33042
|
||||||
#define ID_MSGLOG_COPY 33043
|
#define ID_MSGLOG_COPY 33043
|
||||||
#define ID_MSGLOG_SEARCH 33078
|
#define ID_MSGLOG_SEARCH 33078
|
||||||
|
#define ID_MSGLOG_LOGIN_NOTIFY 33079
|
||||||
#define ID_WEB_REMOTE_CONTROL 33044
|
#define ID_WEB_REMOTE_CONTROL 33044
|
||||||
#define ID_TOOL_PLUGIN_SETTINGS 33045
|
#define ID_TOOL_PLUGIN_SETTINGS 33045
|
||||||
#define ID_33046 33046
|
#define ID_33046 33046
|
||||||
@@ -1032,7 +1033,7 @@
|
|||||||
#ifdef APSTUDIO_INVOKED
|
#ifdef APSTUDIO_INVOKED
|
||||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||||
#define _APS_NEXT_RESOURCE_VALUE 398
|
#define _APS_NEXT_RESOURCE_VALUE 398
|
||||||
#define _APS_NEXT_COMMAND_VALUE 33079
|
#define _APS_NEXT_COMMAND_VALUE 33080
|
||||||
#define _APS_NEXT_CONTROL_VALUE 2542
|
#define _APS_NEXT_CONTROL_VALUE 2542
|
||||||
#define _APS_NEXT_SYMED_VALUE 105
|
#define _APS_NEXT_SYMED_VALUE 105
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
Reference in New Issue
Block a user