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
This commit is contained in:
yuanyuanxiang
2026-08-15 11:09:14 +02:00
parent d0bad75284
commit 29929e48b2
16 changed files with 382 additions and 1 deletions

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"
@@ -1017,6 +1018,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()
@@ -4249,6 +4251,12 @@ 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("历史活动"));
}
// 创建一个新的子菜单
CMenu newMenu;
if (!newMenu.CreatePopupMenu()) {
@@ -5357,6 +5365,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)));
@@ -12015,6 +12033,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);

View File

@@ -650,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();
};

View File

@@ -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>

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 UIMBCS 工程里模板默认字体是 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\nWindows 编辑框换行需要 \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());
}

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
};

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

View File

@@ -1947,6 +1947,7 @@ FRPC Զ
设置失败! 通常是权限不足, 请手动在系统环境变量设置。=设置失败! 通常是权限不足, 请手动在系统环境变量设置。
运行日志=运行日志
客户端日志=客户端日志
历史活动=歷史活動
封禁使用=封禁使用
确定封禁选定的被控程序吗?=确定封禁选定的被控程序吗?
无结果=无结果

View File

@@ -1028,6 +1028,7 @@
#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
@@ -1035,7 +1036,7 @@
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 398
#define _APS_NEXT_COMMAND_VALUE 33082
#define _APS_NEXT_COMMAND_VALUE 33083
#define _APS_NEXT_CONTROL_VALUE 2542
#define _APS_NEXT_SYMED_VALUE 105
#endif