9 Commits
Author SHA1 Message Date
yuanyuanxianganddeepseek-v4-pro 43398e405e Feature: Add icons to online host context menu items
Add 16x16 icons for the three context-menu items that previously had
none: "Activity History" (Client Management), "Browse Window" (Remote
Control), and "Proxy Port - Auto Start" (Client Proxy). Register three
new bitmap resources and extend the main dialog's bitmap array from 63
to 66 entries so each menu item renders an intuitive glyph.

Co-Authored-By: deepseek-v4-pro
2026-08-15 13:42:57 +02:00
yuanyuanxianganddeepseek-v4-pro 29929e48b2 Feature: Add client activity history recording and display
Record the client's active-window history (start time, window title, and
dwell duration), newest first, skipping dwellings under 5 seconds and
breaking the timing on idle, capped at 500 entries. Add an "Activity
History" item inside the host's Client Management submenu that requests
the snapshot over the main connection and shows it in a new dialog. The
dialog rebuilds its edit control as a Unicode window so UTF-8 titles
render correctly instead of degrading to "?" through the MBCS ANSI
boundary. The menu item and dialog title go through _TR and are
localized in en_US and zh_TW.

Co-Authored-By: deepseek-v4-pro
2026-08-15 11:09:14 +02:00
yuanyuanxianganddeepseek-v4-pro d0bad75284 Feature: Remember host list sort preference in main dialog
Persist the sort column and direction chosen via the online host list
header (list\OnlineListSort registry key) so the next launch defaults to
the same sort. Re-apply the sort when hosts come online/offline so newly
connected hosts land in their sorted position instead of appending at the
end.

Co-Authored-By: deepseek-v4-pro
2026-08-13 13:12:29 +02:00
yuanyuanxianganddeepseek-v4-pro 9d2e9ec8ca Feature: Remember process/window list sort preference
Persist the sort column and direction chosen via the list header so the
next time the process or window management dialog opens it defaults to
the same sort. Process and window lists are remembered separately
(list\ProcessListSort and list\WindowListSort registry keys) since their
columns have different meanings.

Co-Authored-By: deepseek-v4-pro
2026-08-13 12:59:29 +02:00
yuanyuanxianganddeepseek-v4-pro 2d7f224a22 Fix: process list architecture column sorting was a no-op
The process list stored the architecture string only in ItemData::Arch,
leaving Data[3] (the architecture column) empty. Sorting by the
architecture column therefore compared empty strings and did nothing.
Populate Data[3] with the architecture value so the sort works.

Co-Authored-By: deepseek-v4-pro
2026-08-13 12:44:14 +02:00
yuanyuanxiangandDeepSeek V4 Pro c8973282cb Feature: Add "Unban IP" to log message context menu in main dialog
- Add "Unban IP" menu item to the log message list (m_CList_Message)
  right-click context menu, performing two actions on the extracted IP:
  1. Remove the IP from the IPBlacklist singleton and persist to config
  2. Scan the entire history host list (m_ClientMap->GetAll()) for all
     clients matching the IP with AUTH_FORBIDDEN status, and restore
     them to UNAUTHORIZED (0) — requiring re-authorization for safety
- Reuses ExtractFirstIPFromMessage() for dependency-free IPv4 parsing
- Handles the case where an IP is not in the blacklist and has no
  matching forbidden clients with a clear "no action needed" message
- Menu item is grayed out when no log entry is selected
- Sync en_US / zh_TW language files with new translation strings

Co-Authored-By: DeepSeek V4 Pro
2026-08-07 11:45:18 +02:00
yuanyuanxiangandDeepSeek V4 Pro 7bbc47a0ee Feature: Add "Ban IP" to log message context menu in main dialog
- Add "Ban IP" 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 directly to the IP blacklist
- Reuses ExtractFirstIPFromMessage() from the previous commit for
  dependency-free IPv4 parsing with per-segment 0-255 validation
- Checks against whitelist before banning (whitelisted IPs cannot be
  banned) and skips IPs already in blacklist with clear feedback
- Immediately persists via THIS_CFG.SetStr to the config ini file;
  IPBlacklist singleton takes effect instantly — subsequent connections
  from banned IPs are rejected in IOCPServer::OnAccept() which already
  calls IsIPBlacklisted() on every new connection
- Menu item is grayed out when no log entry is selected
- Sync en_US / zh_TW language files with new translation strings

Co-Authored-By: DeepSeek V4 Pro
2026-08-06 20:44:33 +02:00
yuanyuanxiangandDeepSeek V4 Pro eeacbe5d61 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
2026-08-06 18:13:14 +02:00
yuanyuanxiangandDeepSeek V4 Pro 7c09e9dd8c Fix: Move ONLINELIST_PRIVILEGE to end of enum to fix history host column data corruption
Commit 6d0b1bb (Feat: Add privilege column) inserted ONLINELIST_PRIVILEGE
at enum value 9, shifting INSTALLTIME from 9→10 and PATH from 12→13.
However, FileUpload_Lib (SimplePlugins) is compiled against a separate
copy of context.h that still uses the old enum values. Since
HostInfo.cpp's SaveClientMapData uses OLD enum values to read
sClientInfo[], it was reading privilegeStr for InstallTime and client
type for ProgramPath — corrupting the history host database.

Fix: move ONLINELIST_PRIVILEGE to value 16 (just before MAX), restoring
all original enum values (INSTALLTIME=9, PATH=12, etc.) so the
pre-built library can continue reading correct data without recompilation.

The 'permission' column now appears as the rightmost column in the
online host list instead of between 'version' and 'install time'.

Co-Authored-By: DeepSeek V4 Pro
2026-08-05 20:07:21 +02:00
26 changed files with 907 additions and 48 deletions

No files matched your search

+126
View File
@@ -0,0 +1,126 @@
// ActivityHistory.cpp: 客户端历史活动记录采集模块
#include "stdafx.h"
#include "ActivityHistory.h"
#include "KernelManager.h" // ActivityWindow
ActivityHistory& ActivityHistory::Instance()
{
static ActivityHistory inst;
return inst;
}
void ActivityHistory::Start()
{
if (m_started.exchange(true))
return;
m_running = true;
m_thread = std::thread(&ActivityHistory::Loop, this);
}
void ActivityHistory::Stop()
{
m_running = false;
if (m_thread.joinable())
m_thread.join();
}
std::string ActivityHistory::Dump() const
{
std::lock_guard<std::mutex> lock(m_mutex);
std::string out;
// 当前进行中的窗口:若已持续达到阈值,作为最新一条返回
if (!m_curTitle.empty() && m_curDwellSec >= ACTIVITY_MIN_DWELL_SEC)
out += FormatRecord(m_curStartTime, m_curTitle, m_curDwellSec) + "\n";
for (const auto& r : m_records)
out += r + "\n";
return out;
}
void ActivityHistory::Loop()
{
// 空闲打断阈值:远大于 ACTIVITY_MIN_DWELL_SEC,避免“停在一个窗口上看几秒”被过早结账。
const DWORD IDLE_THRESHOLD_MS = ACTIVITY_IDLE_BREAK_SEC * 1000;
ActivityWindow aw;
HWND lastHwnd = NULL;
std::string lastTitle;
std::string startTime;
int dwellSec = 0;
while (m_running) {
// 取前台窗口句柄 + 标题;空闲/锁定/取不到标题时 hwnd==NULL 或 title 为空
HWND hwnd = aw.GetActiveWindowHandle(IDLE_THRESHOLD_MS);
std::string title = aw.GetActiveTitleOrEmpty(IDLE_THRESHOLD_MS);
{
std::lock_guard<std::mutex> lock(m_mutex);
if (hwnd == NULL || title.empty()) {
// 空闲/锁定/无标题:结账上一个窗口
if (lastHwnd != NULL && dwellSec >= ACTIVITY_MIN_DWELL_SEC)
m_records.push_front(FormatRecord(startTime, lastTitle, dwellSec));
lastHwnd = NULL;
lastTitle.clear();
dwellSec = 0;
} else if (hwnd == lastHwnd) {
// 同一窗口:即使标题动态变化也连续累计,标题取最新值
++dwellSec;
lastTitle = title;
} else {
// 窗口切换:结账上一个,开始累计新窗口
if (lastHwnd != NULL && dwellSec >= ACTIVITY_MIN_DWELL_SEC)
m_records.push_front(FormatRecord(startTime, lastTitle, dwellSec));
lastHwnd = hwnd;
lastTitle = title;
startTime = NowStr();
dwellSec = 1;
}
while (m_records.size() > ACTIVITY_HISTORY_MAX)
m_records.pop_back();
m_curTitle = lastTitle;
m_curStartTime = startTime;
m_curDwellSec = dwellSec;
}
Sleep(1000);
}
}
std::string ActivityHistory::FormatRecord(const std::string& startTime, const std::string& title, int dwellSec)
{
// 标题里的换行会破坏“每行一条”,做最小清洗
std::string t;
t.reserve(title.size());
for (char c : title) {
if (c == '\r' || c == '\n') c = ' ';
t += c;
}
return "[" + startTime + "] [" + t + "] " + FormatDuration(dwellSec);
}
std::string ActivityHistory::FormatDuration(int sec)
{
if (sec < 0) sec = 0;
if (sec < 60)
return std::to_string(sec) + "s";
if (sec < 3600) {
int m = sec / 60, s = sec % 60;
return s ? std::to_string(m) + "m" + std::to_string(s) + "s"
: std::to_string(m) + "m";
}
int h = sec / 3600, m = (sec % 3600) / 60;
return std::to_string(h) + "h" + std::to_string(m) + "m";
}
std::string ActivityHistory::NowStr()
{
SYSTEMTIME st;
GetLocalTime(&st);
char buf[32];
sprintf_s(buf, sizeof(buf), "%04d-%02d-%02d %02d:%02d:%02d",
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
return buf;
}
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include <string>
#include <deque>
#include <mutex>
#include <thread>
#include <atomic>
// 历史活动记录:
// 每条格式:[YYYY-MM-DD HH:MM:SS] [窗口标题] <时长>
// 倒序存储(最新在前)。后台线程每 1 秒采样一次前台窗口。
#define ACTIVITY_MIN_DWELL_SEC 5 // 一闪而过阈值(秒):驻留低于此值不记录
#define ACTIVITY_IDLE_BREAK_SEC 300 // 空闲打断阈值(秒):空闲超过此值才结账当前窗口
// 注意:与心跳“活动窗口”列的 6s 判定不同,这里要远大于 MIN_DWELL,
// 否则“停在一个窗口上看 10 秒”会被 6s 空闲判定过早结账/漏记
#define ACTIVITY_HISTORY_MAX 500 // 内存保留的最大记录条数
class ActivityHistory
{
public:
static ActivityHistory& Instance();
// 启动常驻采样线程(幂等,重复调用无副作用)
void Start();
// 停止采样线程
void Stop();
// 返回全部历史记录(最新在前,多行文本,每行一条)
std::string Dump() const;
private:
ActivityHistory() = default;
~ActivityHistory() { Stop(); }
ActivityHistory(const ActivityHistory&) = delete;
ActivityHistory& operator=(const ActivityHistory&) = delete;
void Loop();
// 拼一条记录字符串
static std::string FormatRecord(const std::string& startTime, const std::string& title, int dwellSec);
// 时长格式化:Ns / Nm / NmNs / NhNm
static std::string FormatDuration(int sec);
// 当前时间戳 "YYYY-MM-DD HH:MM:SS"
static std::string NowStr();
mutable std::mutex m_mutex;
std::deque<std::string> m_records; // 已完成记录,最新在前
std::string m_curTitle; // 当前进行中的窗口标题
std::string m_curStartTime; // 当前窗口的起始时间
int m_curDwellSec = 0; // 当前窗口已连续活跃秒数
std::atomic<bool> m_running{ false };
std::atomic<bool> m_started{ false };
std::thread m_thread;
};
+2
View File
@@ -3,6 +3,7 @@
#include "stdafx.h"
#include "ClientDll.h"
#include "ActivityHistory.h"
#include <common/iniFile.h>
#include <common/LANChecker.h>
#include <common/VerifyV2.h>
@@ -588,6 +589,7 @@ DWORD WINAPI StartClient(LPVOID lParam)
std::string ip = settings.ServerIP();
int port = settings.ServerPort();
Mprintf("StartClient begin[%s:%d]\n", ip.c_str(), port);
ActivityHistory::Instance().Start();
if (!app.m_bShared) {
auto now = time(0);
valid_to = atof(cfg.GetStr("settings", "valid_to").c_str());
+2
View File
@@ -178,6 +178,7 @@
<ClCompile Include="Audio.cpp" />
<ClCompile Include="AudioManager.cpp" />
<ClCompile Include="Buffer.cpp" />
<ClCompile Include="ActivityHistory.cpp" />
<ClCompile Include="CaptureVideo.cpp" />
<ClCompile Include="clang_rt_compat.c" />
<ClCompile Include="ClientDll.cpp" />
@@ -225,6 +226,7 @@
<ClInclude Include="..\common\zstd_wrapper.h" />
<ClInclude Include="..\server\2015Remote\pwd_gen.h" />
<ClInclude Include="Audio.h" />
<ClInclude Include="ActivityHistory.h" />
<ClInclude Include="AudioManager.h" />
<ClInclude Include="Buffer.h" />
<ClInclude Include="CaptureVideo.h" />
+11
View File
@@ -10,6 +10,7 @@
#include <fstream>
#include <corecrt_io.h>
#include "ClientDll.h"
#include "ActivityHistory.h"
#include "MemoryModule.h"
#include "common/dllRunner.h"
#include "server/2015Remote/pwd_gen.h"
@@ -822,6 +823,16 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL, 0, LoopClientLogManager, &m_hThread[m_ulThreadCount], 0, NULL);
break;
}
case COMMAND_QUERY_ACTIVITY: {
// 主连接一次性快照:直接 dump 历史活动并回传
std::string text = ActivityHistory::Instance().Dump();
std::vector<BYTE> pkt(1 + text.size());
pkt[0] = TOKEN_REPORT_ACTIVITY;
if (!text.empty())
memcpy(pkt.data() + 1, text.data(), text.size());
m_ClientObject->Send2Server((char*)pkt.data(), (ULONG)pkt.size());
break;
}
case CMD_SET_GROUP: {
std::string group = std::string((char*)szBuffer + 1);
m_cfg->SetStr("settings", "group_name", group);
+18
View File
@@ -40,6 +40,24 @@ public:
return (!IsWorkstationLocked() ? "Inactive: " : "Locked: ") + FormatMilliseconds(idle);
}
// 返回当前活跃窗口标题;若空闲(idle ≥ threshold_ms)或取不到标题则返回空串。
// 供历史活动采集使用,避免“判定活跃”与“取标题”之间的竞态窗口。
std::string GetActiveTitleOrEmpty(DWORD threshold_ms = 6000)
{
if (GetUserIdleTime() >= threshold_ms)
return std::string();
return GetActiveWindowTitle();
}
// 返回当前前台窗口句柄;空闲(idle ≥ threshold_ms)返回 NULL。
// 历史活动按窗口(HWND)而非标题字符串累计:标题动态变化时不会反复归零。
HWND GetActiveWindowHandle(DWORD threshold_ms = 6000)
{
if (GetUserIdleTime() >= threshold_ms)
return NULL;
return GetForegroundWindow();
}
private:
std::string FormatMilliseconds(DWORD ms)
{
+2
View File
@@ -185,6 +185,7 @@
<ClCompile Include="..\common\ikcp.c" />
<ClCompile Include="..\common\zstd_wrapper.c" />
<ClCompile Include="..\server\2015Remote\pwd_gen.cpp" />
<ClCompile Include="ActivityHistory.cpp" />
<ClCompile Include="Audio.cpp" />
<ClCompile Include="AudioManager.cpp" />
<ClCompile Include="Buffer.cpp" />
@@ -234,6 +235,7 @@
<ClInclude Include="..\common\wallet.h" />
<ClInclude Include="..\common\zstd_wrapper.h" />
<ClInclude Include="..\server\2015Remote\pwd_gen.h" />
<ClInclude Include="ActivityHistory.h" />
<ClInclude Include="Audio.h" />
<ClInclude Include="AudioManager.h" />
<ClInclude Include="auto_start.h" />
+2
View File
@@ -317,6 +317,8 @@ enum {
TOKEN_REPORT_LOG = 156,
COMMAND_FORBIDDEN = 157,
CMD_CUSTOM_CURSOR = 158,
COMMAND_QUERY_ACTIVITY = 159, // 服务端 → 客户端:索取历史活动
TOKEN_REPORT_ACTIVITY = 160, // 客户端 → 服务端:上报历史活动
TOKEN_DECRYPT = 199,
TOKEN_REGEDIT = 200, // 注册表
Binary file not shown.
+403 -13
View File
@@ -15,6 +15,7 @@
#include "TerminalDlg.h"
#include "SystemDlg.h"
#include "CClientLog.h"
#include "CActivityDialog.h"
#include "BuildDlg.h"
#include "AudioDlg.h"
#include "RegisterDlg.h"
@@ -148,10 +149,14 @@ COLUMNSTRUCT g_Column_Data_Online[g_Column_Count_Online] = {
{"摄像头", 60 },
{"RTT", 60 },
{"版本", 90 },
{"权限", 60 },
{"安装时间", 130 },
{"活动窗口", 130 },
{"类型", 50 },
{"", 0 },
{"", 0 },
{"", 0 },
{"", 0 },
{"权限", 60 },
};
// 缩略图列号常量 + 列号→数据槽映射。负返回值代表"该列无数据槽"。
@@ -648,6 +653,9 @@ CMy2015RemoteDlg::CMy2015RemoteDlg(CWnd* pParent): CDialogLangEx(CMy2015RemoteDl
m_bmOnline[60].LoadBitmap(IDB_BITMAP_DEBUG);
m_bmOnline[61].LoadBitmap(IDB_BITMAP_CLIENTLOG);
m_bmOnline[62].LoadBitmap(IDB_BITMAP_FORBIDDEN);
m_bmOnline[63].LoadBitmap(IDB_BITMAP_HISTORY_ACTIVITY); // 历史活动
m_bmOnline[64].LoadBitmap(IDB_BITMAP_VIEW_WND); // 浏览窗口
m_bmOnline[65].LoadBitmap(IDB_BITMAP_PROXY_AUTORUN); // 代理端口-自启
for (int i = 0; i < PAYLOAD_MAXTYPE; i++) {
m_ServerDLL[i] = nullptr;
m_ServerBin[i] = nullptr;
@@ -938,6 +946,9 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
ON_COMMAND(ID_MSGLOG_COPY, &CMy2015RemoteDlg::OnMsglogCopy)
ON_COMMAND(ID_MSGLOG_CLEAR, &CMy2015RemoteDlg::OnMsglogClear)
ON_COMMAND(ID_MSGLOG_SEARCH, &CMy2015RemoteDlg::OnMsglogSearch)
ON_COMMAND(ID_MSGLOG_LOGIN_NOTIFY, &CMy2015RemoteDlg::OnMsglogLoginNotify)
ON_COMMAND(ID_MSGLOG_BAN_IP, &CMy2015RemoteDlg::OnMsglogBanIP)
ON_COMMAND(ID_MSGLOG_UNBAN_IP, &CMy2015RemoteDlg::OnMsglogUnbanIP)
ON_COMMAND(ID_ONLINE_ADD_WATCH, &CMy2015RemoteDlg::OnOnlineAddWatch)
ON_COMMAND(ID_ONLINE_LOGIN_NOTIFY, &CMy2015RemoteDlg::OnOnlineLoginNotify)
ON_NOTIFY(NM_CUSTOMDRAW, IDC_ONLINE, &CMy2015RemoteDlg::OnNMCustomdrawOnline)
@@ -1010,6 +1021,7 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
ON_COMMAND(ID_ONLINE_VIEW_WND, &CMy2015RemoteDlg::OnOnlineWindowManager)
ON_COMMAND(ID_ENABLE_DEV_DEBUG, &CMy2015RemoteDlg::OnEnableDevDebug)
ON_COMMAND(ID_ONLINE_CLIENT_LOG, &CMy2015RemoteDlg::OnOnlineClientLog)
ON_COMMAND(ID_ONLINE_HISTORY_ACTIVITY, &CMy2015RemoteDlg::OnOnlineHistoryActivity)
ON_COMMAND(ID_ONLINE_FORBIDDEN, &CMy2015RemoteDlg::OnOnlineForbidden)
END_MESSAGE_MAP()
@@ -1412,6 +1424,13 @@ VOID CMy2015RemoteDlg::InitControl()
g_Column_Online_Width+=g_Column_Data_Online[i].nWidth;
}
m_CList_Online.InitColumns();
LoadSortPreference(); // 加载上次记忆的排序偏好
// 隐藏空标题列(内部数据槽,不允许用户拖拽显示)
for (int i = 0; i < g_Column_Count_Online; ++i) {
if (g_Column_Data_Online[i].szTitle[0] == '\0') {
m_CList_Online.SetColumnVisible(i, FALSE);
}
}
// 让虚拟列表用 Unicode 通知(LVN_GETDISPINFOW),从而绕开 MBCS 工程里
// 列表 → ANSI → CP_ACP 的回转,这样在德语/日语等非中文 ACP 服务端上也能显示中文。
m_CList_Online.SendMessage(LVM_SETUNICODEFORMAT, TRUE, 0);
@@ -1487,8 +1506,8 @@ VOID CMy2015RemoteDlg::AddList(CString strIP, CString strAddr, CString strPCName
CString privilegeStr = privUser == "SYSTEM" ? "SYSTEM" : privAdmin == "1" ? "Admin" : "User";
CString data[ONLINELIST_MAX] = { strIP, strAddr, "", strPCName, strOS, strCPU, strVideo, strPing,
verDisplay, privilegeStr, install, startTime, v[RES_CLIENT_TYPE].empty() ? "?" : v[RES_CLIENT_TYPE].c_str(), path,
v[RES_CLIENT_PUBIP].empty() ? strIP : v[RES_CLIENT_PUBIP].c_str(), startTime, capStr,
verDisplay, install, startTime, v[RES_CLIENT_TYPE].empty() ? "?" : v[RES_CLIENT_TYPE].c_str(), path,
v[RES_CLIENT_PUBIP].empty() ? strIP : v[RES_CLIENT_PUBIP].c_str(), startTime, capStr, privilegeStr,
};
// 优先采用客户端自报的 ID(新客户端用 V2 算法 = MachineGuid + 归一化路径,
// 比服务端按老算法 IP+PC+OS+CPU+PATH 重算更稳定)。
@@ -3473,6 +3492,7 @@ void CMy2015RemoteDlg::OnTimer(UINT_PTR nIDEvent)
// 虚拟列表:重建过滤索引并更新项目计数
if (hasListChange) {
ApplySort(); // 有排序偏好时重排,让新上线主机落到排序位置
RebuildFilteredIndices();
m_CList_Online.SetItemCountExV((int)m_FilteredIndices.size(), LVSICF_NOINVALIDATEALL | LVSICF_NOSCROLL);
}
@@ -3867,8 +3887,6 @@ void CMy2015RemoteDlg::SortByColumn(int nColumn)
// 缩略图列点击表头不参与排序 —— 没有可比较的数据维度,避免误触发
if (ColumnToDataSlot(nColumn) < 0) return;
static int m_nSortColumn = 0;
static bool m_bSortAscending = false;
if (nColumn == m_nSortColumn) {
// 如果点击的是同一列,切换排序顺序
m_bSortAscending = !m_bSortAscending;
@@ -3878,8 +3896,25 @@ void CMy2015RemoteDlg::SortByColumn(int nColumn)
m_bSortAscending = true;
}
// 虚拟列表:对数据源进行排序(列号映射到底层数据槽)
EnterCriticalSection(&m_cs);
ApplySort();
RebuildFilteredIndices();
LeaveCriticalSection(&m_cs);
// 刷新列表
m_CList_Online.Invalidate();
SaveSortPreference();
}
void CMy2015RemoteDlg::ApplySort()
{
// 无有效排序偏好或列不合法(缩略图/越界)时保持列表原始顺序
if (m_nSortColumn <= COL_THUMBNAIL || m_nSortColumn >= g_Column_Count_Online) {
return;
}
// 虚拟列表:对数据源进行排序(列号映射到底层数据槽),调用方需持有 m_cs
int slot = ColumnToDataSlot(m_nSortColumn);
bool asc = m_bSortAscending;
std::sort(m_HostList.begin(), m_HostList.end(),
@@ -3890,19 +3925,38 @@ void CMy2015RemoteDlg::SortByColumn(int nColumn)
return asc ? (result < 0) : (result > 0);
});
// 重建索引映射
// 排序后 clientID -> 索引映射已改变,需重建
m_ClientIndex.clear();
for (size_t i = 0; i < m_HostList.size(); ++i) {
if (m_HostList[i]) {
m_ClientIndex[m_HostList[i]->GetClientID()] = i;
}
}
// 重建过滤索引
RebuildFilteredIndices();
LeaveCriticalSection(&m_cs);
}
// 刷新列表
m_CList_Online.Invalidate();
void CMy2015RemoteDlg::LoadSortPreference()
{
std::string val = THIS_CFG.GetStr("list", "OnlineListSort", "");
if (val.empty()) {
m_nSortColumn = -1; // 无偏好:保持列表原始顺序
m_bSortAscending = true;
return;
}
auto arr = StringToVector(val, ',');
if (arr.size() >= 2) {
m_nSortColumn = atoi(arr[0].c_str());
m_bSortAscending = (atoi(arr[1].c_str()) != 0);
} else {
m_nSortColumn = -1;
m_bSortAscending = true;
}
}
void CMy2015RemoteDlg::SaveSortPreference()
{
std::string val = std::to_string(m_nSortColumn) + "," + (m_bSortAscending ? "1" : "0");
THIS_CFG.SetStr("list", "OnlineListSort", val);
}
void CMy2015RemoteDlg::OnHdnItemclickList(NMHDR* pNMHDR, LRESULT* pResult)
@@ -4109,6 +4163,8 @@ void CMy2015RemoteDlg::OnNMRClickOnline(NMHDR *pNMHDR, LRESULT *pResult)
Menu.SetMenuItemBitmaps(ID_ONLINE_ACTIVE_WND, MF_BYCOMMAND, &m_bmOnline[59], &m_bmOnline[59]);
Menu.SetMenuItemBitmaps(ID_ONLINE_CLIENT_LOG, MF_BYCOMMAND, &m_bmOnline[61], &m_bmOnline[61]);
Menu.SetMenuItemBitmaps(ID_ONLINE_FORBIDDEN, MF_BYCOMMAND, &m_bmOnline[62], &m_bmOnline[62]);
Menu.SetMenuItemBitmaps(ID_ONLINE_VIEW_WND, MF_BYCOMMAND, &m_bmOnline[64], &m_bmOnline[64]);
Menu.SetMenuItemBitmaps(ID_PROXY_PORT_AUTORUN, MF_BYCOMMAND, &m_bmOnline[65], &m_bmOnline[65]);
Menu.ModifyMenuL(ID_ONLINE_AUTHORIZE, MF_BYCOMMAND | MF_STRING, ID_ONLINE_AUTHORIZE, _T("发送授权"));
@@ -4200,6 +4256,13 @@ void CMy2015RemoteDlg::OnNMRClickOnline(NMHDR *pNMHDR, LRESULT *pResult)
}
}
// 客户管理子菜单:新增“历史活动”
CMenu* pClientMenu = FindSubMenuByCommand(SubMenu, ID_ONLINE_ASSIGN_TO);
if (pClientMenu) {
pClientMenu->InsertMenu(0, MF_BYPOSITION | MF_STRING, ID_ONLINE_HISTORY_ACTIVITY, _TR("历史活动"));
pClientMenu->SetMenuItemBitmaps(ID_ONLINE_HISTORY_ACTIVITY, MF_BYCOMMAND, &m_bmOnline[63], &m_bmOnline[63]);
}
// 创建一个新的子菜单
CMenu newMenu;
if (!newMenu.CreatePopupMenu()) {
@@ -5308,6 +5371,16 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
}
break;
}
case TOKEN_REPORT_ACTIVITY: {
// 一次性快照:直接打开“历史活动”对话框展示,不占用 hDlg(避免与运行日志对话框冲突)
std::string text((char*)(szBuffer + 1), len > 1 ? len - 1 : 0);
CActivityDialog* dlg = new CActivityDialog(this, ContextObject->GetServer(), ContextObject);
dlg->Create(IDD_DIALOG_CLIENT_LOG, GetDesktopWindow());
dlg->ShowWindow(SW_SHOW);
if (!text.empty())
dlg->AppendLog(text);
break;
}
case TOKEN_CLIENT_MSG: {
ClientMsg *msg =(ClientMsg*)ContextObject->InDeCompressedBuffer.GetBuffer(0);
PostMessageA(WM_SHOWERRORMSG, (WPARAM)new CString(_L(msg->text)), (LPARAM)new CString(_L(msg->title)));
@@ -9293,11 +9366,22 @@ void CMy2015RemoteDlg::OnRClickMessage(NMHDR* pNMHDR, LRESULT* pResult)
menu.AppendMenu(MF_SEPARATOR);
menu.AppendMenu(MF_STRING, ID_MSGLOG_SEARCH,
m_bLogSearchVisible ? _TR("隐藏搜索") : _TR("搜索日志"));
menu.AppendMenu(MF_SEPARATOR);
menu.AppendMenu(MF_STRING, ID_MSGLOG_LOGIN_NOTIFY, _TR("上线提醒"));
menu.AppendMenu(MF_STRING, ID_MSGLOG_BAN_IP, _TR("封禁IP"));
menu.AppendMenu(MF_STRING, ID_MSGLOG_UNBAN_IP, _TR("解除封禁"));
// 没有选中项时禁用"删除选中"
// 没有选中项时禁用"删除选中"、"复制选中"、"上线提醒"、"封禁IP"和"解除封禁"
if (m_CList_Message.GetSelectedCount() == 0) {
menu.EnableMenuItem(ID_MSGLOG_DELETE, MF_GRAYED);
menu.EnableMenuItem(ID_MSGLOG_COPY, MF_GRAYED);
menu.EnableMenuItem(ID_MSGLOG_LOGIN_NOTIFY, MF_GRAYED);
menu.EnableMenuItem(ID_MSGLOG_BAN_IP, MF_GRAYED);
menu.EnableMenuItem(ID_MSGLOG_UNBAN_IP, MF_GRAYED);
}
// PowerShell不可用时禁用"上线提醒"
if (!GetNotifyManager().IsPowerShellAvailable()) {
menu.EnableMenuItem(ID_MSGLOG_LOGIN_NOTIFY, MF_GRAYED);
}
// 列表为空时禁用"清空日志"
if (m_CList_Message.GetItemCount() == 0) {
@@ -9369,6 +9453,306 @@ void CMy2015RemoteDlg::OnMsglogSearch()
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::OnMsglogBanIP()
{
// 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;
}
// Process each extracted IP
int bannedCount = 0;
int alreadyCount = 0;
int whitelistCount = 0;
for (const auto& ip : ipsToAdd) {
// Cannot ban whitelisted IPs
if (IPWhitelist::getInstance().IsWhitelisted(ip)) {
whitelistCount++;
continue;
}
// Already in blacklist
if (IPBlacklist::getInstance().IsBlacklisted(ip)) {
alreadyCount++;
continue;
}
IPBlacklist::getInstance().AddIP(ip);
bannedCount++;
}
// Persist to config
if (bannedCount > 0) {
std::string blacklistStr = IPBlacklist::getInstance().Export();
THIS_CFG.SetStr("settings", "IPBlacklist", blacklistStr.c_str());
}
// Build result message
CString details;
if (bannedCount > 0) {
details.AppendFormat(_TR("已封禁 %d 个 IP"), bannedCount);
}
if (alreadyCount > 0) {
if (!details.IsEmpty()) details += _T("\n");
details.AppendFormat(_TR("%d 个 IP 已在黑名单中"), alreadyCount);
}
if (whitelistCount > 0) {
if (!details.IsEmpty()) details += _T("\n");
details.AppendFormat(_TR("%d 个 IP 在白名单中,已跳过"), whitelistCount);
}
if (details.IsEmpty()) {
details = _TR("未执行任何操作");
}
MessageBoxL(details, _TR("封禁IP"), MB_ICONINFORMATION);
}
void CMy2015RemoteDlg::OnMsglogUnbanIP()
{
// Get selected log messages and extract IP addresses from them
std::vector<std::string> ipsToProcess;
POSITION pos = m_CList_Message.GetFirstSelectedItemPosition();
while (pos) {
int row = m_CList_Message.GetNextSelectedItem(pos);
CString msgText = m_CList_Message.GetItemText(row, 2);
CString extractedIP;
if (ExtractFirstIPFromMessage(msgText, extractedIP)) {
std::string ipUtf8 = CT2A(extractedIP, CP_UTF8);
ipsToProcess.push_back(ipUtf8);
}
}
if (ipsToProcess.empty()) {
MessageBoxL("所选日志消息中未检测到有效的 IP 地址", "提示", MB_ICONINFORMATION);
return;
}
int unblacklistedCount = 0;
int clientRestoredCount = 0;
for (const auto& ip : ipsToProcess) {
// 1. Remove from IP blacklist
if (IPBlacklist::getInstance().IsBlacklisted(ip)) {
IPBlacklist::getInstance().RemoveIP(ip);
unblacklistedCount++;
}
// 2. Find all clients matching this IP and restore AUTH_FORBIDDEN to UNAUTHORIZED
for (const auto& kv : m_ClientMap->GetAll()) {
if (strcmp(kv.second.IP, ip.c_str()) == 0
&& kv.second.Authorized == AUTH_FORBIDDEN) {
m_ClientMap->SetClientMapInteger(kv.first, MAP_AUTH, UNAUTHORIZED);
clientRestoredCount++;
}
}
}
// Persist
if (unblacklistedCount > 0) {
std::string blacklistStr = IPBlacklist::getInstance().Export();
THIS_CFG.SetStr("settings", "IPBlacklist", blacklistStr.c_str());
}
if (clientRestoredCount > 0) {
m_ClientMap->SaveToFile(GetDbPath());
}
// Build result message
CString details;
if (unblacklistedCount > 0) {
details.AppendFormat(_TR("已将 %d 个 IP 从黑名单移除"), unblacklistedCount);
}
if (clientRestoredCount > 0) {
if (!details.IsEmpty()) details += _T("\n");
details.AppendFormat(_TR("已恢复 %d 个客户端的授权状态"), clientRestoredCount);
}
if (unblacklistedCount == 0 && clientRestoredCount == 0) {
details = _TR("未找到需要解除封禁的 IP");
}
MessageBoxL(details, _TR("解除封禁"), MB_ICONINFORMATION);
}
void CMy2015RemoteDlg::OnOnlineAddWatch()
{
EnterCriticalSection(&m_cs);
@@ -11655,6 +12039,12 @@ void CMy2015RemoteDlg::OnOnlineClientLog()
SendSelectedCommand(&cmd, sizeof(BYTE));
}
void CMy2015RemoteDlg::OnOnlineHistoryActivity()
{
BYTE cmd = COMMAND_QUERY_ACTIVITY;
SendSelectedCommand(&cmd, sizeof(BYTE));
}
LRESULT CMy2015RemoteDlg::OnOpenClientLogDialog(WPARAM wParam, LPARAM lParam)
{
std::string* pInitLog = reinterpret_cast<std::string*>(wParam);
+10 -1
View File
@@ -173,6 +173,9 @@ protected:
DECLARE_MESSAGE_MAP()
public:
void SortByColumn(int nColumn);
void ApplySort(); // 按偏好重排 m_HostList(调用方需持有 m_cs)
void LoadSortPreference();
void SaveSortPreference();
afx_msg VOID OnHdnItemclickList(NMHDR* pNMHDR, LRESULT* pResult);
static int CALLBACK CompareFunction(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort);
template<class T, int id, int Show = SW_SHOW> LRESULT OpenDialog(WPARAM wParam, LPARAM lParam)
@@ -342,6 +345,8 @@ public:
std::vector<context*> m_HostList; // 虚拟列表数据源(全部客户端)
std::unordered_map<uint64_t, size_t> m_ClientIndex; // clientID -> m_HostList 索引映射
std::vector<size_t> m_FilteredIndices; // 当前分组过滤后的索引列表
int m_nSortColumn = -1; // 当前排序列,-1 表示未排序
bool m_bSortAscending = true; // 排序方向(true=升序)
std::set<std::string> m_GroupList;
std::string m_selectedGroup;
std::string m_v2KeyPath; // V2 密钥文件路径
@@ -385,7 +390,7 @@ public:
bool IsDllRequestLimited(const std::string& ip);
void RecordDllRequest(const std::string& ip);
CMenu m_MainMenu;
CBitmap m_bmOnline[63];
CBitmap m_bmOnline[66];
uint64_t m_superID;
std::map<HWND, CDialogBase *> m_RemoteWnds;
FileTransformCmd m_CmdList;
@@ -564,6 +569,9 @@ public:
afx_msg void OnMsglogCopy();
afx_msg void OnMsglogClear();
afx_msg void OnMsglogSearch();
afx_msg void OnMsglogLoginNotify();
afx_msg void OnMsglogBanIP();
afx_msg void OnMsglogUnbanIP();
afx_msg void OnOnlineAddWatch();
afx_msg void OnNMCustomdrawOnline(NMHDR* pNMHDR, LRESULT* pResult);
afx_msg void OnOnlineRunAsAdmin();
@@ -642,6 +650,7 @@ public:
afx_msg void OnOnlineActiveWnd();
afx_msg void OnEnableDevDebug();
afx_msg void OnOnlineClientLog();
afx_msg void OnOnlineHistoryActivity();
afx_msg LRESULT OnOpenClientLogDialog(WPARAM wParam, LPARAM lParam);
afx_msg void OnOnlineForbidden();
};
@@ -291,6 +291,7 @@
<ClInclude Include="BuildDlg.h" />
<ClInclude Include="CClientListDlg.h" />
<ClInclude Include="CClientLog.h" />
<ClInclude Include="CActivityDialog.h" />
<ClInclude Include="LogSearchBar.h" />
<ClInclude Include="CDlgFileSend.h" />
<ClInclude Include="CDrawingBoard.h" />
@@ -374,6 +375,7 @@
<ItemGroup>
<ClCompile Include="..\..\client\Audio.cpp" />
<ClCompile Include="CClientLog.cpp" />
<ClCompile Include="CActivityDialog.cpp" />
<ClCompile Include="LogSearchBar.cpp" />
<ClCompile Include="msvc_compat.c">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
+98
View File
@@ -0,0 +1,98 @@
// CActivityDialog.cpp: 历史活动展示对话框(一次性快照,无子连接)
#include "stdafx.h"
#include "afxdialogex.h"
#include "CActivityDialog.h"
#include "resource.h"
#include "../../common/commands.h"
IMPLEMENT_DYNAMIC(CActivityDialog, CDialogEx)
CActivityDialog::CActivityDialog(CWnd* pParent, Server* pServer, CONTEXT_OBJECT* pContext)
: CDialogBase(IDD_DIALOG_CLIENT_LOG, pParent, pServer, pContext, 0)
{
}
CActivityDialog::~CActivityDialog()
{
}
void CActivityDialog::DoDataExchange(CDataExchange* pDX)
{
CDialogBase::DoDataExchange(pDX);
DDX_Control(pDX, IDC_EDIT_LOG, m_editLog);
}
BEGIN_MESSAGE_MAP(CActivityDialog, CDialogBase)
ON_WM_SIZE()
END_MESSAGE_MAP()
BOOL CActivityDialog::OnInitDialog()
{
CDialogBase::OnInitDialog();
SetWindowText(_TR("历史活动") + " - " + m_IPAddress);
HICON hIcon = AfxGetApp()->LoadIcon(IDI_CLIENTLOG);
SetIcon(hIcon, TRUE);
SetIcon(hIcon, FALSE);
// 用 Segoe UI:MBCS 工程里模板默认字体是 Microsoft Sans Serif(无 Dingbats 字形),
// 显式指定 Segoe UI,中文走字体链接到微软雅黑、Dingbats(✻/✦)走 Segoe UI Symbol 回退。
m_font.CreatePointFont(90, _T("Segoe UI"));
m_editLog.SetFont(&m_font);
// 重建为 Unicode 编辑框:这是 ✻/✦ 能正确显示的关键。重建时已保留上面的字体。
RebuildEdit(m_editLog);
return TRUE;
}
void CActivityDialog::RebuildEdit(CEdit& m_edit)
{
CRect rc;
m_edit.GetWindowRect(&rc);
ScreenToClient(&rc);
DWORD style = m_edit.GetStyle();
DWORD exStyle = m_edit.GetExStyle();
HFONT hFont = (HFONT)m_edit.SendMessage(WM_GETFONT, 0, 0);
UINT ctrlID = m_edit.GetDlgCtrlID();
m_edit.DestroyWindow();
HWND hEdit = ::CreateWindowExW(
exStyle, L"EDIT", L"", style,
rc.left, rc.top, rc.Width(), rc.Height(),
this->GetSafeHwnd(), (HMENU)(UINT_PTR)ctrlID,
AfxGetInstanceHandle(), NULL);
m_edit.Attach(hEdit);
if (hFont)
m_edit.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
}
void CActivityDialog::OnSize(UINT nType, int cx, int cy)
{
CDialogBase::OnSize(nType, cx, cy);
if (m_editLog.GetSafeHwnd() && cx > 0 && cy > 0)
m_editLog.MoveWindow(7, 7, cx - 14, cy - 14);
}
// 快照模式:无子连接,OnReceiveComplete 空实现(纯虚函数必须覆盖)
void CActivityDialog::OnReceiveComplete()
{
}
void CActivityDialog::AppendLog(const std::string& text)
{
if (text.empty()) return;
// \n → \r\n,Windows 编辑框换行需要 \r\n
std::string norm;
norm.reserve(text.size() + 64);
for (size_t i = 0; i < text.size(); ++i) {
if (text[i] == '\n' && (i == 0 || text[i - 1] != '\r'))
norm += '\r';
norm += text[i];
}
// 服务端是 MBCS 工程,编辑框走 A 接口会把 UTF-8 中文当 ANSI 处理导致乱码;
// 与"活动窗口"列一致,用 W 接口显示:先把 UTF-8 转成宽字符,再走 WM_SETTEXTW 写入。
// (编辑框已在 OnInitDialog 中重建为 Unicode 窗口,SetWindowTextW 不会经过 CP_ACP 回转。)
int wlen = MultiByteToWideChar(CP_UTF8, 0, norm.c_str(), (int)norm.size(), NULL, 0);
if (wlen <= 0) return;
std::wstring w(wlen, L'\0');
MultiByteToWideChar(CP_UTF8, 0, norm.c_str(), (int)norm.size(), &w[0], wlen);
::SetWindowTextW(m_editLog.GetSafeHwnd(), w.c_str());
}
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include "afxcmn.h"
#include "IOCPServer.h"
class CActivityDialog : public CDialogBase
{
DECLARE_DYNAMIC(CActivityDialog)
public:
CActivityDialog(CWnd* pParent, Server* pServer, CONTEXT_OBJECT* pContext);
virtual ~CActivityDialog();
virtual void OnReceiveComplete();
void AppendLog(const std::string& text);
protected:
virtual BOOL OnInitDialog();
virtual void DoDataExchange(CDataExchange* pDX);
afx_msg void OnSize(UINT nType, int cx, int cy);
DECLARE_MESSAGE_MAP()
private:
// 把编辑框重建为 Unicode 类窗口(同 CKeyBoardDlg::RebuildEdit)。
// 工程是 MBCS,对话框模板创建的是 ANSI 编辑框,SetWindowTextW 会在 W->A
// 边界用 CP_ACP 转码,Dingbats(✻/✦)等字符会变 ?。重建为 Unicode 后
// W 版消息直通,不再走 CP_ACP。
void RebuildEdit(CEdit& m_edit);
CEdit m_editLog;
CFont m_font; // 编辑框字体(成员持有,随对话框销毁,避免堆泄漏)
#ifdef AFX_DESIGN_TIME
enum { IDD = IDD_DIALOG_CLIENT_LOG };
#endif
};
+8 -4
View File
@@ -104,10 +104,14 @@ inline const char* GetColumnName(int index) {
"Camera", // 6
"RTT", // 7
"Version", // 8
"Privilege", // 9 ONLINELIST_PRIVILEGE
"InstallTime", // 10 ONLINELIST_INSTALLTIME
"ActiveWindow", // 11 ONLINELIST_LOGINTIME
"ClientType", // 12 ONLINELIST_CLIENTTYPE
"InstallTime", // 9 ONLINELIST_INSTALLTIME
"ActiveWindow", // 10 ONLINELIST_LOGINTIME
"ClientType", // 11 ONLINELIST_CLIENTTYPE
"Path", // 12 ONLINELIST_PATH
"PublicIP", // 13 ONLINELIST_PUBIP
"StartTime", // 14 ONLINELIST_STARTTIME
"Capabilities", // 15 ONLINELIST_CAPABILITIES
"Privilege", // 16 ONLINELIST_PRIVILEGE
};
if (index >= 0 && index < sizeof(names)/sizeof(names[0])) {
return names[index];
+39 -22
View File
@@ -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;
if (rule.columnIndex == ONLINELIST_COMPUTER_NAME && !remark.IsEmpty()) {
colText = remark;
} else {
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
std::string colTextStr = CT2A(colText, CP_UTF8);
// Split pattern by semicolon and check each keyword
std::vector<std::string> keywords = SplitString(rule.matchPattern, ';');
for (const auto& kw : keywords) {
std::string trimmed = Trim(kw);
if (trimmed.empty()) continue;
// 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;
}
// Also check the IP column (ONLINELIST_IP) so IP keywords added from log
// messages can match against host IP addresses when they come online.
CString ipText = ctx->GetClientData(ONLINELIST_IP);
matched = matchColumn(ipText);
if (!matched.empty()) {
outMatchedKeyword = matched;
m_config.lastNotifyTime[clientId] = now;
return true;
}
return false;
+1 -1
View File
@@ -111,10 +111,10 @@ void NotifySettingsDlg::PopulateComboBoxes()
{ ONLINELIST_VIDEO, _T("摄像头") },
{ ONLINELIST_PING, _T("延迟") },
{ ONLINELIST_VERSION, _T("版本") },
{ ONLINELIST_PRIVILEGE, _T("权限") },
{ ONLINELIST_INSTALLTIME, _T("安装时间") },
{ ONLINELIST_LOGINTIME, _T("活动窗口") },
{ ONLINELIST_CLIENTTYPE, _T("客户端类型") },
{ ONLINELIST_PRIVILEGE, _T("权限") },
};
for (const auto& col : columns) {
CString item;
+52 -4
View File
@@ -80,6 +80,7 @@ BOOL CSystemDlg::OnInitDialog()
SetWindowText(str);//设置对话框标题
m_ControlList.SetExtendedStyle(LVS_EX_FLATSB | LVS_EX_FULLROWSELECT);
LoadSortPreference(); // 加载上次记忆的排序偏好
if (m_bHow==TOKEN_PSLIST) { //进程管理初始化列表
m_ControlList.InsertColumnL(0, "映像名称", LVCFMT_LEFT, 180);
m_ControlList.InsertColumnL(1, "PID", LVCFMT_LEFT, 70);
@@ -227,6 +228,8 @@ void CSystemDlg::ShowWindowsList(void)
lvc.pszText = str.GetBuffer(0);
lvc.cchTextMax = str.GetLength();
m_ControlList.SetColumn(1, &lvc);
ApplySort(); // 应用记忆的排序偏好
}
void CSystemDlg::ShowProcessList(void)
@@ -260,8 +263,8 @@ void CSystemDlg::ShowProcessList(void)
m_ControlList.SetItemText(i, 1, str);
m_ControlList.SetItemText(i, 2, szProcessFullPath);
m_ControlList.SetItemText(i, 3, arr[1].empty() ? "N/A" : arr[1].c_str());
// ItemData 为进程ID
auto data = new ItemData{ *PID, {arr[0].c_str(), str, szProcessFullPath}, arr[1].c_str() };
// ItemData 为进程ID;Data[3] 需与第 4 列"架构"一致,否则按架构排序拿到空串而无意义
auto data = new ItemData{ *PID, {arr[0].c_str(), str, szProcessFullPath, arr[1].empty() ? "N/A" : arr[1].c_str()}, arr[1].c_str() };
m_ControlList.SetItemData(i, DWORD_PTR(data));
dwOffset += sizeof(DWORD) + lstrlen(szExeFile) + lstrlen(szProcessFullPath) + 2; //跳过这个数据结构 进入下一个循环
@@ -273,6 +276,8 @@ void CSystemDlg::ShowProcessList(void)
lvc.pszText = str.GetBuffer(0);
lvc.cchTextMax = str.GetLength();
m_ControlList.SetColumn(2, &lvc); //在列表中显示有多少个进程
ApplySort(); // 应用记忆的排序偏好
}
@@ -319,8 +324,6 @@ int CALLBACK CSystemDlg::CompareFunction(LPARAM lParam1, LPARAM lParam2, LPARAM
void CSystemDlg::SortByColumn(int nColumn)
{
static int m_nSortColumn = 0;
static bool m_bSortAscending = false;
if (nColumn == m_nSortColumn) {
// 如果点击的是同一列,切换排序顺序
m_bSortAscending = !m_bSortAscending;
@@ -330,11 +333,56 @@ void CSystemDlg::SortByColumn(int nColumn)
m_bSortAscending = true;
}
ApplySort();
SaveSortPreference();
}
void CSystemDlg::ApplySort()
{
// 无有效排序偏好时保持列表原始顺序
if (m_nSortColumn < 0) {
return;
}
// 防御:配置被手动修改导致列索引越界时跳过排序
CHeaderCtrl* pHeader = m_ControlList.GetHeaderCtrl();
if (pHeader && m_nSortColumn >= pHeader->GetItemCount()) {
return;
}
// 创建排序信息
std::pair<int, bool> sortInfo(m_nSortColumn, m_bSortAscending);
m_ControlList.SortItems(CompareFunction, reinterpret_cast<LPARAM>(&sortInfo));
}
void CSystemDlg::LoadSortPreference()
{
// 进程管理 / 窗口管理 分别记忆各自的排序偏好
const char* key = (m_bHow == TOKEN_PSLIST) ? "ProcessListSort" : "WindowListSort";
std::string val = THIS_CFG.GetStr("list", key, "");
if (val.empty()) {
m_nSortColumn = -1; // 无偏好:保持列表原始顺序
m_bSortAscending = true;
return;
}
auto arr = StringToVector(val, ',');
if (arr.size() >= 2) {
m_nSortColumn = atoi(arr[0].c_str());
m_bSortAscending = (atoi(arr[1].c_str()) != 0);
} else {
m_nSortColumn = -1;
m_bSortAscending = true;
}
}
void CSystemDlg::SaveSortPreference()
{
const char* key = (m_bHow == TOKEN_PSLIST) ? "ProcessListSort" : "WindowListSort";
std::string val = std::to_string(m_nSortColumn) + "," + (m_bSortAscending ? "1" : "0");
THIS_CFG.SetStr("list", key, val);
}
void CSystemDlg::OnHdnItemclickList(NMHDR* pNMHDR, LRESULT* pResult)
{
LPNMHEADER pNMHeader = reinterpret_cast<LPNMHEADER>(pNMHDR);
+5
View File
@@ -19,6 +19,8 @@ public:
void OnReceiveComplete(void);
CWnd* m_pParent;
BOOL m_bHow;
int m_nSortColumn = -1; // 当前排序列,-1 表示未排序
bool m_bSortAscending = true; // 排序方向(true=升序)
// 对话框数据
enum { IDD = IDD_DIALOG_SYSTEM };
@@ -32,6 +34,9 @@ public:
void DeleteAllItems();
void SortByColumn(int nColumn);
void ApplySort();
void LoadSortPreference();
void SaveSortPreference();
afx_msg VOID OnHdnItemclickList(NMHDR* pNMHDR, LRESULT* pResult);
static int CALLBACK CompareFunction(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort);
+1 -1
View File
@@ -14,7 +14,6 @@ enum {
ONLINELIST_VIDEO, // 摄像头(有无)
ONLINELIST_PING, // PING(对方的网速)
ONLINELIST_VERSION, // 版本信息
ONLINELIST_PRIVILEGE, // 权限级别 (SYSTEM/Admin/User)
ONLINELIST_INSTALLTIME, // 安装时间
ONLINELIST_LOGINTIME, // 活动窗口
ONLINELIST_CLIENTTYPE, // 客户端类型
@@ -22,6 +21,7 @@ enum {
ONLINELIST_PUBIP,
ONLINELIST_STARTTIME,
ONLINELIST_CAPABILITIES, // 客户端能力位
ONLINELIST_PRIVILEGE, // 权限级别 (SYSTEM/Admin/User)
ONLINELIST_MAX,
};
+13
View File
@@ -1956,6 +1956,7 @@ FRPC Զ
设置失败! 通常是权限不足, 请手动在系统环境变量设置。=Failed! You have to set it via system environment center.
运行日志=Client Log
客户端日志=Client Log
历史活动=Activity History
封禁使用=Forbidden Client
确定封禁选定的被控程序吗?=Are you sure to forbidden the selected clients?
无结果=Not found
@@ -1965,3 +1966,15 @@ FRPC Զ
搜索日志=Search Logs
隐藏搜索=Hide Search
请=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
封禁IP=Ban IP
已封禁 %d 个 IP=Banned %d IP(s)
%d 个 IP 已在黑名单中=%d IP(s) already in blacklist
%d 个 IP 在白名单中,已跳过=%d IP(s) in whitelist, skipped
未执行任何操作=No action taken
解除封禁=Unban IP
已将 %d 个 IP 从黑名单移除=Removed %d IP(s) from blacklist
已恢复 %d 个客户端的授权状态=Restored authorization for %d client(s)
未找到需要解除封禁的 IP=No IPs found that need unbanning
+13
View File
@@ -1947,6 +1947,7 @@ FRPC Զ
设置失败! 通常是权限不足, 请手动在系统环境变量设置。=设置失败! 通常是权限不足, 请手动在系统环境变量设置。
运行日志=运行日志
客户端日志=客户端日志
历史活动=歷史活動
封禁使用=封禁使用
确定封禁选定的被控程序吗?=确定封禁选定的被控程序吗?
无结果=无结果
@@ -1956,3 +1957,15 @@ FRPC Զ
搜索日志=搜索日志
隐藏搜索=隐藏搜索
请=请
所有解析出的 IP 已在上线提醒列表中=所有解析出的 IP 已在上線提醒列表中
已添加 %d 个 IP 到上线提醒列表=已新增 %d 個 IP 到上線提醒列表
所选日志消息中未检测到有效的 IP 地址=所選日誌消息中未檢測到有效的 IP 位址
封禁IP=封禁IP
已封禁 %d 个 IP=已封禁 %d 個 IP
%d 个 IP 已在黑名单中=%d 個 IP 已在黑名單中
%d 个 IP 在白名单中,已跳过=%d 個 IP 在白名單中,已跳過
未执行任何操作=未執行任何操作
解除封禁=解除封禁
已将 %d 个 IP 从黑名单移除=已將 %d 個 IP 從黑名單移除
已恢复 %d 个客户端的授权状态=已恢復 %d 個客戶端的授權狀態
未找到需要解除封禁的 IP=未找到需要解除封禁的 IP
Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

+9 -2
View File
@@ -277,6 +277,9 @@
#define IDB_BITMAP_CLIENTLOG 395
#define IDI_CLIENTLOG 396
#define IDB_BITMAP_FORBIDDEN 397
#define IDB_BITMAP_HISTORY_ACTIVITY 398
#define IDB_BITMAP_VIEW_WND 399
#define IDB_BITMAP_PROXY_AUTORUN 400
#define IDC_MESSAGE 1000
#define IDC_ONLINE 1001
#define IDC_STATIC_TIPS 1002
@@ -991,6 +994,9 @@
#define ID_CANCEL_SHARE 33042
#define ID_MSGLOG_COPY 33043
#define ID_MSGLOG_SEARCH 33078
#define ID_MSGLOG_LOGIN_NOTIFY 33079
#define ID_MSGLOG_BAN_IP 33080
#define ID_MSGLOG_UNBAN_IP 33081
#define ID_WEB_REMOTE_CONTROL 33044
#define ID_TOOL_PLUGIN_SETTINGS 33045
#define ID_33046 33046
@@ -1025,14 +1031,15 @@
#define ID_ONLINE_CLIENT_LOG 33074
#define ID_ONLINE_33075 33075
#define ID_ONLINE_FORBIDDEN 33076
#define ID_ONLINE_HISTORY_ACTIVITY 33082
#define ID_EXIT_FULLSCREEN 40001
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 398
#define _APS_NEXT_COMMAND_VALUE 33079
#define _APS_NEXT_RESOURCE_VALUE 401
#define _APS_NEXT_COMMAND_VALUE 33083
#define _APS_NEXT_CONTROL_VALUE 2542
#define _APS_NEXT_SYMED_VALUE 105
#endif