Compare commits
19
Commits
v1.3.8
..
11e8b122fe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11e8b122fe | ||
|
|
0a1ecfc4ab | ||
|
|
c08315cd79 | ||
|
|
5611ba621c | ||
|
|
c6c6e1d5ef | ||
|
|
61089e5fae | ||
|
|
bcde74368d | ||
|
|
6ba6b0289d | ||
|
|
86c4f14cf4 | ||
|
|
13052b7cae | ||
|
|
43398e405e | ||
|
|
29929e48b2 | ||
|
|
d0bad75284 | ||
|
|
9d2e9ec8ca | ||
|
|
2d7f224a22 | ||
|
|
c8973282cb | ||
|
|
7bbc47a0ee | ||
|
|
eeacbe5d61 | ||
|
|
7c09e9dd8c |
No files matched your search
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include "stdafx.h"
|
#include "stdafx.h"
|
||||||
#include "ClientDll.h"
|
#include "ClientDll.h"
|
||||||
|
#include "ActivityHistory.h"
|
||||||
#include <common/iniFile.h>
|
#include <common/iniFile.h>
|
||||||
#include <common/LANChecker.h>
|
#include <common/LANChecker.h>
|
||||||
#include <common/VerifyV2.h>
|
#include <common/VerifyV2.h>
|
||||||
@@ -588,6 +589,7 @@ DWORD WINAPI StartClient(LPVOID lParam)
|
|||||||
std::string ip = settings.ServerIP();
|
std::string ip = settings.ServerIP();
|
||||||
int port = settings.ServerPort();
|
int port = settings.ServerPort();
|
||||||
Mprintf("StartClient begin[%s:%d]\n", ip.c_str(), port);
|
Mprintf("StartClient begin[%s:%d]\n", ip.c_str(), port);
|
||||||
|
ActivityHistory::Instance().Start();
|
||||||
if (!app.m_bShared) {
|
if (!app.m_bShared) {
|
||||||
auto now = time(0);
|
auto now = time(0);
|
||||||
valid_to = atof(cfg.GetStr("settings", "valid_to").c_str());
|
valid_to = atof(cfg.GetStr("settings", "valid_to").c_str());
|
||||||
|
|||||||
@@ -178,6 +178,7 @@
|
|||||||
<ClCompile Include="Audio.cpp" />
|
<ClCompile Include="Audio.cpp" />
|
||||||
<ClCompile Include="AudioManager.cpp" />
|
<ClCompile Include="AudioManager.cpp" />
|
||||||
<ClCompile Include="Buffer.cpp" />
|
<ClCompile Include="Buffer.cpp" />
|
||||||
|
<ClCompile Include="ActivityHistory.cpp" />
|
||||||
<ClCompile Include="CaptureVideo.cpp" />
|
<ClCompile Include="CaptureVideo.cpp" />
|
||||||
<ClCompile Include="clang_rt_compat.c" />
|
<ClCompile Include="clang_rt_compat.c" />
|
||||||
<ClCompile Include="ClientDll.cpp" />
|
<ClCompile Include="ClientDll.cpp" />
|
||||||
@@ -225,6 +226,7 @@
|
|||||||
<ClInclude Include="..\common\zstd_wrapper.h" />
|
<ClInclude Include="..\common\zstd_wrapper.h" />
|
||||||
<ClInclude Include="..\server\2015Remote\pwd_gen.h" />
|
<ClInclude Include="..\server\2015Remote\pwd_gen.h" />
|
||||||
<ClInclude Include="Audio.h" />
|
<ClInclude Include="Audio.h" />
|
||||||
|
<ClInclude Include="ActivityHistory.h" />
|
||||||
<ClInclude Include="AudioManager.h" />
|
<ClInclude Include="AudioManager.h" />
|
||||||
<ClInclude Include="Buffer.h" />
|
<ClInclude Include="Buffer.h" />
|
||||||
<ClInclude Include="CaptureVideo.h" />
|
<ClInclude Include="CaptureVideo.h" />
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <corecrt_io.h>
|
#include <corecrt_io.h>
|
||||||
#include "ClientDll.h"
|
#include "ClientDll.h"
|
||||||
|
#include "ActivityHistory.h"
|
||||||
#include "MemoryModule.h"
|
#include "MemoryModule.h"
|
||||||
#include "common/dllRunner.h"
|
#include "common/dllRunner.h"
|
||||||
#include "server/2015Remote/pwd_gen.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);
|
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL, 0, LoopClientLogManager, &m_hThread[m_ulThreadCount], 0, NULL);
|
||||||
break;
|
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: {
|
case CMD_SET_GROUP: {
|
||||||
std::string group = std::string((char*)szBuffer + 1);
|
std::string group = std::string((char*)szBuffer + 1);
|
||||||
m_cfg->SetStr("settings", "group_name", group);
|
m_cfg->SetStr("settings", "group_name", group);
|
||||||
|
|||||||
@@ -40,6 +40,24 @@ public:
|
|||||||
return (!IsWorkstationLocked() ? "Inactive: " : "Locked: ") + FormatMilliseconds(idle);
|
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:
|
private:
|
||||||
std::string FormatMilliseconds(DWORD ms)
|
std::string FormatMilliseconds(DWORD ms)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -185,6 +185,7 @@
|
|||||||
<ClCompile Include="..\common\ikcp.c" />
|
<ClCompile Include="..\common\ikcp.c" />
|
||||||
<ClCompile Include="..\common\zstd_wrapper.c" />
|
<ClCompile Include="..\common\zstd_wrapper.c" />
|
||||||
<ClCompile Include="..\server\2015Remote\pwd_gen.cpp" />
|
<ClCompile Include="..\server\2015Remote\pwd_gen.cpp" />
|
||||||
|
<ClCompile Include="ActivityHistory.cpp" />
|
||||||
<ClCompile Include="Audio.cpp" />
|
<ClCompile Include="Audio.cpp" />
|
||||||
<ClCompile Include="AudioManager.cpp" />
|
<ClCompile Include="AudioManager.cpp" />
|
||||||
<ClCompile Include="Buffer.cpp" />
|
<ClCompile Include="Buffer.cpp" />
|
||||||
@@ -234,6 +235,7 @@
|
|||||||
<ClInclude Include="..\common\wallet.h" />
|
<ClInclude Include="..\common\wallet.h" />
|
||||||
<ClInclude Include="..\common\zstd_wrapper.h" />
|
<ClInclude Include="..\common\zstd_wrapper.h" />
|
||||||
<ClInclude Include="..\server\2015Remote\pwd_gen.h" />
|
<ClInclude Include="..\server\2015Remote\pwd_gen.h" />
|
||||||
|
<ClInclude Include="ActivityHistory.h" />
|
||||||
<ClInclude Include="Audio.h" />
|
<ClInclude Include="Audio.h" />
|
||||||
<ClInclude Include="AudioManager.h" />
|
<ClInclude Include="AudioManager.h" />
|
||||||
<ClInclude Include="auto_start.h" />
|
<ClInclude Include="auto_start.h" />
|
||||||
|
|||||||
@@ -317,6 +317,8 @@ enum {
|
|||||||
TOKEN_REPORT_LOG = 156,
|
TOKEN_REPORT_LOG = 156,
|
||||||
COMMAND_FORBIDDEN = 157,
|
COMMAND_FORBIDDEN = 157,
|
||||||
CMD_CUSTOM_CURSOR = 158,
|
CMD_CUSTOM_CURSOR = 158,
|
||||||
|
COMMAND_QUERY_ACTIVITY = 159, // 服务端 → 客户端:索取历史活动
|
||||||
|
TOKEN_REPORT_ACTIVITY = 160, // 客户端 → 服务端:上报历史活动
|
||||||
|
|
||||||
TOKEN_DECRYPT = 199,
|
TOKEN_DECRYPT = 199,
|
||||||
TOKEN_REGEDIT = 200, // 注册表
|
TOKEN_REGEDIT = 200, // 注册表
|
||||||
|
|||||||
+7
-1
@@ -152,6 +152,12 @@ public:
|
|||||||
id + "[" + timestamp + "] [" + file + ":" + std::to_string(line) + "] " + message:
|
id + "[" + timestamp + "] [" + file + ":" + std::to_string(line) + "] " + message:
|
||||||
id + "[" + timestamp + "] " + message;
|
id + "[" + timestamp + "] " + message;
|
||||||
|
|
||||||
|
// 方案B:在源头统一保证每条日志以单个 '\n' 结尾,文件与内存 ring buffer
|
||||||
|
// 共用同一份文本;写文件时不再额外追加换行,内存导出直接拼接即可正确分行。
|
||||||
|
while (!logEntry.empty() && (logEntry.back() == '\n' || logEntry.back() == '\r'))
|
||||||
|
logEntry.pop_back();
|
||||||
|
logEntry += '\n';
|
||||||
|
|
||||||
// Always record to in-memory ring buffer regardless of enable flag.
|
// Always record to in-memory ring buffer regardless of enable flag.
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(m_memMutex);
|
std::lock_guard<std::mutex> lock(m_memMutex);
|
||||||
@@ -305,7 +311,7 @@ private:
|
|||||||
std::lock_guard<std::mutex> lock(fileMutex);
|
std::lock_guard<std::mutex> lock(fileMutex);
|
||||||
std::ofstream logFile(logFileName, std::ios::app);
|
std::ofstream logFile(logFileName, std::ios::app);
|
||||||
if (logFile.is_open()) {
|
if (logFile.is_open()) {
|
||||||
logFile << logEntry << std::endl;
|
logFile << logEntry; // logEntry 已在 log() 中以单个 '\n' 结尾
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,443 @@
|
|||||||
|
# YAMA 服务端 MCP 支持方案
|
||||||
|
|
||||||
|
> 版本:1.0
|
||||||
|
> 状态:Final(定稿)
|
||||||
|
> 适用范围:`server/2015Remote/`(C++/MFC 服务端,生产主流)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 背景与目标
|
||||||
|
|
||||||
|
### 1.1 背景
|
||||||
|
|
||||||
|
MCP(Model Context Protocol)是 Anthropic 提出的开放协议,让 AI 助手(Claude Code、Claude Desktop 等)通过标准化的工具调用接口访问外部系统的能力。
|
||||||
|
|
||||||
|
YAMA 服务端维护了「在线主机列表」这一核心数据,如果能通过 MCP 暴露出去,用户就能在 Claude Code 里直接问「现在有多少台机器在线?」「列出所有在线的 Windows 主机」等,由 AI 调用工具获取实时数据。
|
||||||
|
|
||||||
|
### 1.2 本期范围(MVP)
|
||||||
|
|
||||||
|
**只跑通 MCP,只实现一个工具**,把可扩展架构搭起来:
|
||||||
|
|
||||||
|
- 传输方式:Streamable HTTP(`POST /mcp`,JSON-RPC 2.0 over HTTP)
|
||||||
|
- 工具数量:1 个 —— `list_online_hosts`(获取在线主机列表)
|
||||||
|
- 暴露范围:启用开关(默认关闭,经「扩展 → MCP设置」对话框开启)+ 绑定地址可配置(默认 `127.0.0.1` 仅本机)+ token 认证(env / THIS_CFG / 随机)
|
||||||
|
- 后续:逐步添加工具(进程列表、命令执行、屏幕截图等),架构不动
|
||||||
|
|
||||||
|
### 1.3 非目标(明确不做)
|
||||||
|
|
||||||
|
- 不做 SSE 服务器推送(本期工具调用是 request/response 即可)
|
||||||
|
- 不做多用户/角色体系(复用现有 WebService 认证或静态 token)
|
||||||
|
- 不做写操作工具(操控主机的工具属于后续阶段,需单独评审安全边界)
|
||||||
|
- 不碰现有 Web 控制台的 WebSocket 服务
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 现状分析
|
||||||
|
|
||||||
|
C++ 服务端已有大量可直接复用的资产,MCP 的落地成本因此大幅降低。
|
||||||
|
|
||||||
|
| 现成资产 | 位置 | 在 MCP 中的角色 |
|
||||||
|
|---|---|---|
|
||||||
|
| HTTP 服务器 | `httplib.h`(header-only,`file_server.h:34` 已用) | MCP 的 HTTP 传输层 |
|
||||||
|
| JSON 库 | `jsoncpp/json.h`(`WebService.cpp` 已用) | JSON-RPC 编解码 |
|
||||||
|
| Token 认证 | `WebServiceAuth.h`(`GenerateToken` / `ValidateToken` / `ComputeSHA256`) | 可选升级路径;MVP 用简单静态 token 比对(见 §3.5) |
|
||||||
|
| 在线主机列表 → JSON | `CWebService::BuildDeviceListJson()`(`WebService.cpp:1506`) | **工具 `list_online_hosts` 的核心逻辑** |
|
||||||
|
| 在线主机数据源 | `CMy2015RemoteDlg::m_HostList`(`2015RemoteDlg.h:345`,`m_cs` 保护)+ `context::IsLogin()` + `context::GetClientData()` | 取数入口 |
|
||||||
|
| 配置系统 | `THIS_CFG.GetInt/GetStr("settings", ...)` | MCP 端口/token 配置 |
|
||||||
|
|
||||||
|
### 2.1 关键结论
|
||||||
|
|
||||||
|
- **HTTP、JSON、认证、主机列表 JSON 全部现成**,MCP 侧不需要重新造轮子。
|
||||||
|
- **唯一需要手写的是 MCP 的 JSON-RPC 2.0 协议层**,因为 C++ 没有官方 MCP SDK(官方仅有 TS/Python/Java/Kotlin/C#/Go)。
|
||||||
|
|
||||||
|
### 2.2 `BuildDeviceListJson` 现状
|
||||||
|
|
||||||
|
`WebService.cpp:1506` 的 `CWebService::BuildDeviceListJson(const std::string& username)` 已实现:
|
||||||
|
|
||||||
|
- 遍历 `m_pParentDlg->m_HostList`,过滤 `ctx->IsLogin()`
|
||||||
|
- 输出字段:`id` / `name` / `remark` / `ip` / `os` / `location` / `rtt` / `version` / `activeWindow` / `online` / `group` / `screen` / `clientType`
|
||||||
|
- 已处理 GBK/UTF-8 编码分叉(`activeWindow` 字段按客户端能力位选择编码页,见 `WebService.cpp:1581` 附近的 `GetClientEncoding`)
|
||||||
|
|
||||||
|
**该方法是 `private`**(`WebService.h:160`,位于 private 段)。MCP 侧需通过公开入口复用(见 §3.4)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 方案设计
|
||||||
|
|
||||||
|
### 3.1 总体架构
|
||||||
|
|
||||||
|
```
|
||||||
|
2015Remote.exe(MFC 服务端进程)
|
||||||
|
├─ IOCP TCP 服务(被控端连接,现有)
|
||||||
|
├─ WebService(ws::Server,Web 控制台,现有,8080)
|
||||||
|
└─ McpServer(httplib,新增,默认禁用,经「MCP设置」开启后监听 127.0.0.1:6544)
|
||||||
|
POST /mcp → JSON-RPC 分发
|
||||||
|
├─ initialize
|
||||||
|
├─ tools/list
|
||||||
|
└─ tools/call
|
||||||
|
└─ list_online_hosts → 复用 BuildHostJson(公共函数,§3.4)
|
||||||
|
```
|
||||||
|
|
||||||
|
**核心决策:独立起一个 httplib server,不挂到现有 `ws::Server`。**
|
||||||
|
|
||||||
|
理由:
|
||||||
|
- 现有 `ws::Server` 的 `HttpHandler` 只有 `path` 参数、无 POST body(`SimpleWebSocket.h:345`),强挂 MCP 需改动其底层,侵入大、有回归风险。
|
||||||
|
- 独立 server 零耦合、零风险,且天然满足「仅 127.0.0.1」的绑定需求。
|
||||||
|
|
||||||
|
### 3.2 新增文件清单
|
||||||
|
|
||||||
|
| 文件 | 职责 | 估算量 |
|
||||||
|
|---|---|---|
|
||||||
|
| `McpServer.h` / `McpServer.cpp` | httplib 封装、`POST /mcp` 路由、token 校验、生命周期 | ~120 行 |
|
||||||
|
| `McpProtocol.h` / `McpProtocol.cpp` | JSON-RPC 2.0 协议层:`initialize` / `tools/list` / `tools/call` 分发 | ~250 行 |
|
||||||
|
| `McpTools.h` / `McpTools.cpp` | 工具注册表 + `list_online_hosts` 实现 | ~80 行 |
|
||||||
|
| `McpSettingsDlg.h` / `McpSettingsDlg.cpp` | 「MCP设置」配置对话框(启用/端口/绑定/token + 重启提醒) | ~150 行 |
|
||||||
|
| 修改 `2015RemoteDlg.cpp` / `.h` | 启动/停止 McpServer + 菜单项接线(`ID_MCP_SETTINGS` → `OnMcpSettings`) | ~30 行 |
|
||||||
|
| 修改 `2015Remote.rc` / `resource.h` / `UIBranding.h` / `FeatureFlags.h` | `ID_MCP_SETTINGS` 菜单项 + `IDD_DIALOG_MCP_SETTINGS` 对话框资源 + `HIDE_MENU_MCP_SETTINGS` + `MF_MCP_SETTINGS` 许可位 | 少量 |
|
||||||
|
|
||||||
|
> 头文件/实现可合并精简,实际以「一个 `McpServer` 类 + 一个工具注册表 + 一个协议分发函数」为最小形态,文件拆分仅为可读性。
|
||||||
|
|
||||||
|
### 3.3 协议层设计(JSON-RPC 2.0)
|
||||||
|
|
||||||
|
MCP 是 JSON-RPC 2.0。MVP 只需实现以下方法:
|
||||||
|
|
||||||
|
#### 3.3.1 `initialize`(握手)
|
||||||
|
|
||||||
|
请求:
|
||||||
|
```json
|
||||||
|
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"claude","version":"..."}}}
|
||||||
|
```
|
||||||
|
|
||||||
|
响应:
|
||||||
|
```json
|
||||||
|
{"jsonrpc":"2.0","id":1,"result":{
|
||||||
|
"protocolVersion":"2025-06-18",
|
||||||
|
"capabilities":{"tools":{}},
|
||||||
|
"serverInfo":{"name":"yama","version":"<VERSION_STR>"}
|
||||||
|
}}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.3.2 `tools/list`
|
||||||
|
|
||||||
|
请求:`{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}`
|
||||||
|
|
||||||
|
响应:
|
||||||
|
```json
|
||||||
|
{"jsonrpc":"2.0","id":2,"result":{"tools":[{
|
||||||
|
"name":"list_online_hosts",
|
||||||
|
"description":"获取当前所有在线主机的列表,包含计算机名、IP、操作系统、版本、备注、分组、活动窗口、延迟等实时信息。",
|
||||||
|
"inputSchema":{"type":"object","properties":{},"required":[]},
|
||||||
|
"outputSchema":{
|
||||||
|
"type":"object",
|
||||||
|
"properties":{
|
||||||
|
"hosts":{
|
||||||
|
"type":"array",
|
||||||
|
"items":{
|
||||||
|
"type":"object",
|
||||||
|
"properties":{
|
||||||
|
"id":{"type":"string"},
|
||||||
|
"name":{"type":"string"},
|
||||||
|
"remark":{"type":"string"},
|
||||||
|
"ip":{"type":"string"},
|
||||||
|
"os":{"type":"string"},
|
||||||
|
"location":{"type":"string"},
|
||||||
|
"rtt":{"type":"string"},
|
||||||
|
"version":{"type":"string"},
|
||||||
|
"activeWindow":{"type":"string"},
|
||||||
|
"online":{"type":"boolean"},
|
||||||
|
"group":{"type":"string"},
|
||||||
|
"screen":{"type":"string"},
|
||||||
|
"clientType":{"type":"string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required":["hosts"]
|
||||||
|
}
|
||||||
|
}]}}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.3.3 `tools/call`
|
||||||
|
|
||||||
|
请求:
|
||||||
|
```json
|
||||||
|
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"list_online_hosts","arguments":{}}}
|
||||||
|
```
|
||||||
|
|
||||||
|
响应(结构化输出:`structuredContent` 承载主机数组,`content` 附带可读摘要):
|
||||||
|
```json
|
||||||
|
{"jsonrpc":"2.0","id":3,"result":{
|
||||||
|
"structuredContent":{
|
||||||
|
"hosts":[
|
||||||
|
{"id":"123","name":"DESKTOP-ABC","remark":"我的Windows","ip":"192.168.0.92",
|
||||||
|
"os":"Windows 10","location":"上海市","rtt":"12","version":"...",
|
||||||
|
"activeWindow":"WeChat","online":true,"group":"default","screen":"1:1920x1080","clientType":"EXE"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"content":[{"type":"text","text":"共 1 台主机在线。"}],
|
||||||
|
"isError":false
|
||||||
|
}}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.3.4 可选实现
|
||||||
|
|
||||||
|
- `ping`:返回空 result(便于健康检查)
|
||||||
|
- `notifications/initialized`:客户端通知,可忽略
|
||||||
|
|
||||||
|
#### 3.3.5 错误响应(JSON-RPC 2.0 标准)
|
||||||
|
|
||||||
|
协议层必须处理以下错误,返回标准 `error` 结构(`id` 回填请求 id):
|
||||||
|
|
||||||
|
| 场景 | code | message |
|
||||||
|
|---|---|---|
|
||||||
|
| 请求体不是合法 JSON | `-32700` | Parse error |
|
||||||
|
| 请求结构不合法(缺 jsonrpc/method 等) | `-32600` | Invalid Request |
|
||||||
|
| 方法未实现 | `-32601` | Method not found |
|
||||||
|
| 参数非法 | `-32602` | Invalid params |
|
||||||
|
| 内部错误 | `-32603` | Internal error |
|
||||||
|
|
||||||
|
示例(方法未实现):
|
||||||
|
```json
|
||||||
|
{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"Method not found"}}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.3.6 传输层约定(Streamable HTTP 最小子集)
|
||||||
|
|
||||||
|
- 客户端 `POST /mcp`,请求头 `Content-Type: application/json`、`Accept: application/json, text/event-stream`。
|
||||||
|
- 服务端返回 `Content-Type: application/json`(MVP 工具调用为 request/response,无需 SSE 流式)。
|
||||||
|
- `initialize` 响应可回填 `Mcp-Session-Id` 头用于会话管理;MVP 单会话可省略,实现时对照最新 MCP 规范确认。
|
||||||
|
- 仅接受 `POST /mcp`,其余路径/方法返回 404/405。
|
||||||
|
|
||||||
|
### 3.4 工具设计:`list_online_hosts`
|
||||||
|
|
||||||
|
#### 取数方案(已定:抽公共函数)
|
||||||
|
|
||||||
|
`BuildDeviceListJson` 目前是 `CWebService` 的 private 方法,且依赖 `m_pParentDlg`。为满足「MCP 与 WebService 是平级数据消费者、不互相依赖」的原则,采用**方案 C:抽公共函数**。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 公共函数:提取单台在线主机的完整数据(含 GBK/UTF-8 编码处理),返回 UTF-8 的 Json::Value
|
||||||
|
Json::Value BuildHostJson(context* ctx, _ClientList* clientMap);
|
||||||
|
```
|
||||||
|
|
||||||
|
- `CWebService::BuildDeviceListJson` 内部改调该公共函数,Web 侧签名与输出不变(零回归)。
|
||||||
|
- `McpServer` 侧持 `CMy2015RemoteDlg*`(`SetParentDlg`),在 `m_cs` 锁内遍历 `m_HostList`、`IsLogin()` 过滤,对每台主机调 `BuildHostJson`,组装为结构化输出。
|
||||||
|
- **职责边界**:用户名/分组过滤(`username` → `allowed_groups`)、`m_cs` 加锁、以及外层结构(Web 的 `{"cmd","devices"}` vs MCP 的 `{"hosts"}`)属于**调用方**,不进公共函数;`BuildHostJson` 只负责「单台主机 → `Json::Value`」的字段序列化与编码转换。其中 `remark` 依赖 `m_ClientMap->GetClientMapData(id, MAP_NOTE)`,故 `clientMap` 作为参数传入。
|
||||||
|
|
||||||
|
选型说明:方案 A(MCP 依赖 `CWebService` 单例)依赖方向错误,且 `WebSvrPort=0` 时 `m_pParentDlg` 为空导致取不到数;方案 B(MCP 自持父对话框复制逻辑)违反 DRY、重复易错的编码处理。故取 C。
|
||||||
|
|
||||||
|
#### 返回内容(全量,含实时信息)
|
||||||
|
|
||||||
|
**保留 `BuildDeviceListJson` 的全部字段,不做精简**,以支持「AI 协助监督客户端运行状态」的场景:
|
||||||
|
|
||||||
|
- 基础标识:`id` / `name` / `remark` / `ip` / `os` / `location` / `group` / `version` / `clientType` / `screen`
|
||||||
|
- 实时信息:`rtt`(延迟)、`activeWindow`(当前活动窗口,即被控端正在做什么)
|
||||||
|
|
||||||
|
输出结构 `{"hosts": [...]}`,每台主机一个对象,字段与 §3.3.2 的 `outputSchema` 一致。
|
||||||
|
|
||||||
|
### 3.5 安全设计
|
||||||
|
|
||||||
|
- **绑定地址**:默认 `127.0.0.1`(仅本机回环,其他机器无法访问);可经 `McpBind` 配置为 `0.0.0.0`(所有网卡)或内网 IP(如 `192.168.0.92`)以支持远程调用。
|
||||||
|
- 非回环绑定时 token 经明文 HTTP 传输,需强 token,并建议配合防火墙限制源 IP 或启用 HTTPS(项目已有 WebHTTPS 实践)。
|
||||||
|
- **静态 token 校验**:每个 `POST /mcp` 请求校验 `Authorization: Bearer <token>` header;不匹配返回 HTTP 401。
|
||||||
|
- **token 来源**(优先顺序):环境变量 `YAMA_MCP_TOKEN` → `THIS_CFG` 的 `McpToken` → 两者皆空则**随机生成**。
|
||||||
|
- 前两者任一非空即作为固定 token;仅当两者皆空时才随机生成(仅本次进程运行期间有效,进程重启后重新生成不同的 token)。
|
||||||
|
- 随机生成时**必须将 token 输出到日志**(`Mprintf`),否则用户无从获知、无法在 Claude Code 侧配置。
|
||||||
|
- 环境变量名 `YAMA_MCP_TOKEN` 建议同 `BRAND_WEB_ENV_VAR` 一样在 `UIBranding.h` 定义为宏(如 `BRAND_MCP_ENV_VAR`),避免散落硬编码字符串。
|
||||||
|
- **只读边界**:MVP 仅一个只读工具,不暴露任何写/操控能力。
|
||||||
|
- **HTTP 方法限制**:仅接受 `POST /mcp`,其余返回 404/405。
|
||||||
|
|
||||||
|
### 3.6 配置设计
|
||||||
|
|
||||||
|
沿用现有 `THIS_CFG`(`settings` 节):
|
||||||
|
|
||||||
|
| 配置键 | 默认值 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `McpEnabled` | `0` | MCP 总开关:`0` = 禁用(默认),`1` = 启用。经「扩展 → MCP设置」对话框设置 |
|
||||||
|
| `McpPort` | `6544` | MCP 监听端口;仅当 `McpEnabled=1` 时生效;未配置用默认 `6544` |
|
||||||
|
| `McpBind` | `127.0.0.1` | 监听地址;默认仅本机;可设 `0.0.0.0`(所有网卡)或内网 IP(如 `192.168.0.92`)以支持远程 |
|
||||||
|
| `McpToken` | 空(UI 默认随机值) | 静态 token;对话框内「token 必填、默认随机值」——首次打开若为空则预填随机并随保存持久化。运行时优先级:env `YAMA_MCP_TOKEN` → `McpToken` → 皆空则随机生成(见 §3.5) |
|
||||||
|
|
||||||
|
> 与旧版「`McpPort=0` 即禁用」不同:启用与否改由独立的 `McpEnabled` 开关控制,`McpPort` 回归纯端口语义,二者解耦。
|
||||||
|
|
||||||
|
### 3.7 生命周期接入
|
||||||
|
|
||||||
|
在 `2015RemoteDlg.cpp` Web 服务启动块(约 `2048-2081`)之后,新增 MCP 启动逻辑。`McpServer` 与 `CWebService` 同为单例:提供 `static McpServer& Instance()` 与全局 `inline McpServer& McpServer()` 访问器(仿 `WebService()`,见 `WebService.h` 末尾的全局访问器写法)。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
auto mcpEnabled = THIS_CFG.GetInt("settings", "McpEnabled", 0); // 默认禁用
|
||||||
|
auto mcpPort = THIS_CFG.GetInt("settings", "McpPort", 6544);
|
||||||
|
auto mcpBind = THIS_CFG.GetStr("settings", "McpBind", "127.0.0.1");
|
||||||
|
if (mcpEnabled) {
|
||||||
|
// token 优先顺序:env YAMA_MCP_TOKEN → THIS_CFG McpToken → 皆空则随机(仅本次进程有效)
|
||||||
|
const char* envTok = getenv("YAMA_MCP_TOKEN");
|
||||||
|
std::string mcpToken;
|
||||||
|
if (envTok && *envTok) {
|
||||||
|
mcpToken = envTok;
|
||||||
|
} else {
|
||||||
|
mcpToken = THIS_CFG.GetStr("settings", "McpToken", "");
|
||||||
|
}
|
||||||
|
if (mcpToken.empty()) {
|
||||||
|
mcpToken = GenerateRandomToken(); // 32 hex 随机串
|
||||||
|
Mprintf("[McpServer] YAMA_MCP_TOKEN / McpToken 均未设置,本次进程随机 token:%s\n", mcpToken.c_str());
|
||||||
|
}
|
||||||
|
McpServer().SetParentDlg(this); // 方案 C:MCP 需遍历 m_HostList,须持父对话框指针
|
||||||
|
McpServer().SetToken(mcpToken);
|
||||||
|
if (!McpServer().Start(mcpBind, mcpPort)) {
|
||||||
|
Mprintf("McpServer start failed on %s:%d\n", mcpBind.c_str(), mcpPort);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
并在退出路径(`OnDestroy` / `ExitInstance` 对应位置)调用 `McpServer().Stop()`。
|
||||||
|
|
||||||
|
### 3.8 「MCP设置」配置对话框
|
||||||
|
|
||||||
|
MCP 默认不启用,通过主对话框「扩展 → MCP设置」菜单项打开配置对话框来开启。菜单项位置:`2015Remote.rc` 扩展菜单(`POPUP "扩展(&X)"`)内、「地理信息(&L)」子菜单(纯真数据库 / IP2Region)的 `END` 之后、「插件设置」之前。
|
||||||
|
|
||||||
|
#### 3.8.1 菜单与资源接线
|
||||||
|
|
||||||
|
1. **`resource.h`**:新增命令 ID(取当前空闲段,经查 `33077` 空闲):
|
||||||
|
```cpp
|
||||||
|
#define ID_MCP_SETTINGS 33077
|
||||||
|
```
|
||||||
|
2. **`2015Remote.rc`**:在「地理信息(&L)」子菜单 `END` 之后、`插件设置` 之前插入:
|
||||||
|
```
|
||||||
|
MENUITEM "MCP设置(&M)...", ID_MCP_SETTINGS
|
||||||
|
```
|
||||||
|
并新增对话框资源 `IDD_DIALOG_MCP_SETTINGS`(「启用 MCP」复选框 + 端口/绑定地址/token 三个编辑框 + 确定/取消按钮)。
|
||||||
|
3. **`UIBranding.h`**:扩展菜单隐藏开关新增一行(默认 `0` 显示):
|
||||||
|
```cpp
|
||||||
|
#define HIDE_MENU_MCP_SETTINGS 0 // MCP设置
|
||||||
|
```
|
||||||
|
4. **`FeatureFlags.h`**:新增运行时许可位,占用 MenuFlags 保留段 `[43-63]` 的首位(紧跟 `MF_REQUEST_AUTH` 之后):
|
||||||
|
```cpp
|
||||||
|
#define MF_MCP_SETTINGS (1ULL << 43) // HIDE_MENU_MCP_SETTINGS
|
||||||
|
```
|
||||||
|
5. **`2015RemoteDlg.cpp`**:`#include "McpSettingsDlg.h"`,然后消息映射 + 处理器 + 剪枝:
|
||||||
|
```cpp
|
||||||
|
ON_COMMAND(ID_MCP_SETTINGS, &CMy2015RemoteDlg::OnMcpSettings)
|
||||||
|
// ...
|
||||||
|
void CMy2015RemoteDlg::OnMcpSettings()
|
||||||
|
{
|
||||||
|
CMcpSettingsDlg dlg(this);
|
||||||
|
dlg.DoModal();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
并在扩展菜单剪枝块(`pExtMenu`,约 `2015RemoteDlg.cpp:1225` 之后)追加:
|
||||||
|
```cpp
|
||||||
|
if (SHOULD_HIDE_MENU(HIDE_MENU_MCP_SETTINGS, MF_MCP_SETTINGS))
|
||||||
|
pExtMenu->DeleteMenu(ID_MCP_SETTINGS, MF_BYCOMMAND);
|
||||||
|
```
|
||||||
|
|
||||||
|
> **实现注意**:`2015Remote.rc` 为 UTF-16 编码,菜单与对话框资源须由 VS 资源编辑器维护(手工改文本易破坏编码与资源 ID 引用)。`IDD_DIALOG_MCP_SETTINGS` 需在 `resource.h` 分配独立空闲的 `IDD_` 数值(与 `ID_MCP_SETTINGS` 分开);对话框内控件 ID(如 `IDC_CHK_ENABLE_MCP` / `IDC_EDIT_MCP_PORT` / `IDC_EDIT_MCP_BIND` / `IDC_EDIT_MCP_TOKEN`)同理。
|
||||||
|
|
||||||
|
#### 3.8.2 对话框字段与默认值
|
||||||
|
|
||||||
|
| 控件 | 绑定配置键 | 默认值 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 「启用 MCP」复选框 | `McpEnabled` | 未勾选(`0`) | 勾选后持久化 `McpEnabled=1` |
|
||||||
|
| 端口 | `McpPort` | `6544` | 数字编辑框,仅当启用时有效 |
|
||||||
|
| 绑定地址 | `McpBind` | `127.0.0.1` | 默认仅本机;可改 `0.0.0.0` / 内网 IP |
|
||||||
|
| Token | `McpToken` | 随机值 | 必填;为空时预填随机 token |
|
||||||
|
|
||||||
|
#### 3.8.3 初始化与保存
|
||||||
|
|
||||||
|
打开时读 `THIS_CFG` 回填;token 为空则当场生成随机值预填(32 hex)。保存时落盘并弹重启提醒:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
BOOL CMcpSettingsDlg::OnInitDialog()
|
||||||
|
{
|
||||||
|
CDialogLangEx::OnInitDialog();
|
||||||
|
int enabled = THIS_CFG.GetInt("settings", "McpEnabled", 0);
|
||||||
|
int port = THIS_CFG.GetInt("settings", "McpPort", 6544);
|
||||||
|
std::string bind = THIS_CFG.GetStr("settings", "McpBind", "127.0.0.1");
|
||||||
|
std::string tok = THIS_CFG.GetStr("settings", "McpToken", "");
|
||||||
|
if (tok.empty()) tok = GenerateRandomToken(); // 预填随机,随保存持久化
|
||||||
|
// 回填到控件:m_bEnabled / m_nPort / m_strBind / m_strToken
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMcpSettingsDlg::OnBnClickedBtnSave()
|
||||||
|
{
|
||||||
|
// 从控件收集:enabled / port / bind / token
|
||||||
|
if (enabled && token.empty()) {
|
||||||
|
MessageBoxL(_TR("Token 不能为空"), _TR("提示"), MB_ICONWARNING);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
THIS_CFG.SetInt("settings", "McpEnabled", enabled ? 1 : 0);
|
||||||
|
THIS_CFG.SetInt("settings", "McpPort", port);
|
||||||
|
THIS_CFG.SetStr("settings", "McpBind", bind);
|
||||||
|
THIS_CFG.SetStr("settings", "McpToken", token);
|
||||||
|
MessageBoxL(_TR("MCP 设置已保存。\n启用/端口/绑定地址/Token 的改动需重启程序生效。"),
|
||||||
|
_TR("提示"), MB_ICONINFORMATION);
|
||||||
|
CDialogLangEx::OnOK();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.8.4 与运行时 token 逻辑的衔接
|
||||||
|
|
||||||
|
对话框的「默认随机 token」是把随机值**写入 `McpToken` 并持久化**,于是下次启动走 `env → McpToken` 的固定 token 分支;运行时「两者皆空则随机」分支仅在用户从未通过对话框设置过(或手动清空 token)时兜底,两者不冲突。`GenerateRandomToken()` 为 MCP 侧新增的小工具函数(32 hex 随机串,可用 `rand_s` / `BCryptGenRandom` 实现;如需可复用 `WebServiceAuth.h` 的 `ComputeSHA256` 派生)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 实施步骤
|
||||||
|
|
||||||
|
| 步骤 | 内容 | 验收 |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | 新建 `McpProtocol`:JSON-RPC 分发 + `initialize`/`tools/list`/`tools/call`(含 jsoncpp 解析/序列化) | 单元自测:手写请求串,验证响应 |
|
||||||
|
| 2 | 新建 `McpServer`:httplib 起 `127.0.0.1:6544`,`POST /mcp` 路由 + token 校验(含随机生成) | curl 请求返回 401/正常 |
|
||||||
|
| 3 | 抽公共函数 `BuildHostJson`,`CWebService::BuildDeviceListJson` 改调之 | Web 输出不变,零回归 |
|
||||||
|
| 4 | 注册 `list_online_hosts` 工具:handler 持父对话框、`m_cs` 锁内遍历、调 `BuildHostJson` 组装 | `tools/call` 返回主机列表 |
|
||||||
|
| 5 | 新增 `McpSettingsDlg` + 菜单项接线(`ID_MCP_SETTINGS` / `OnMcpSettings` / `HIDE_MENU_MCP_SETTINGS` / `IDD_DIALOG_MCP_SETTINGS` 资源) | 对话框可启用/禁用、配置端口/绑定/token,保存落盘并提示重启 |
|
||||||
|
| 6 | 接入 `2015RemoteDlg` 启动/停止(按 `McpEnabled` 门控)+ 配置 | 服务端启动后端口监听正常 |
|
||||||
|
| 7 | Claude Code 接入验证(见 §5) | 见验收标准 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 验收标准
|
||||||
|
|
||||||
|
1. 在「MCP设置」对话框启用并重启服务端后,`127.0.0.1:6544` 监听正常,外部地址无法访问;未启用时该端口不监听。
|
||||||
|
2. 无 token 的请求返回 HTTP 401;错误 token 返回 401;正确 token 通过。
|
||||||
|
3. `curl` 模拟 `initialize` → `tools/list` → `tools/call` 全流程,返回符合 JSON-RPC 2.0 与 MCP 规范。
|
||||||
|
4. Claude Code 侧:
|
||||||
|
```bash
|
||||||
|
claude mcp add yama --transport http http://127.0.0.1:6544/mcp -H "Authorization: Bearer <token>"
|
||||||
|
```
|
||||||
|
`claude mcp list` 能看到 `yama`,在对话中问「列出在线主机」能正确触发工具并返回列表。
|
||||||
|
5. 现有 Web 控制台、IOCP 主服务功能无回归。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 风险与约束
|
||||||
|
|
||||||
|
| 风险 | 说明 | 应对 |
|
||||||
|
|---|---|---|
|
||||||
|
| 无官方 C++ MCP SDK | 需手写 JSON-RPC 协议层 | 仅实现 3 个方法,量小可控;协议稳定后再考虑抽库 |
|
||||||
|
| MCP 规范版本演进 | protocolVersion 字符串需与客户端匹配 | 实现时对照最新规范;`initialize` 返回可协商版本 |
|
||||||
|
| 并发安全 | 遍历 `m_HostList` 需持 `m_cs` | MCP 与 WebService 采用相同的「`m_cs` 锁内遍历」模式 |
|
||||||
|
| 字符编码 | GBK/UTF-8 分叉 | 复用公共函数 `BuildHostJson` 的编码处理,不自造 |
|
||||||
|
| token 存储 | `THIS_CFG` 的 token 会明文落盘;随机 token 会打印到日志 | 本地回环 + 只读,风险可控;敏感场景建议仅用 env 并妥善保管 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 后续扩展规划(本期不做)
|
||||||
|
|
||||||
|
- 工具:`get_host_processes`(进程列表)、`execute_command`(命令执行)、`capture_screen`(截图)等
|
||||||
|
- 写操作工具的安全边界评审(操作对象、权限、审计)
|
||||||
|
- 认证升级:复用 `WebServiceAuth` 的签名 token / 过期机制
|
||||||
|
- 分组过滤:`list_online_hosts` 加 `group` 参数
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 决策记录
|
||||||
|
|
||||||
|
**决策记录(全部已定):**
|
||||||
|
- ✅ **启用开关**:新增 `McpEnabled`,默认 `0`(禁用)。MCP 默认不启动,须用户在「扩展 → MCP设置」对话框手动开启。
|
||||||
|
- ✅ **端口**:`McpPort` 可配置,未配置默认 `6544`(纯端口语义,不再兼任启用开关)。
|
||||||
|
- ✅ **token**:优先顺序 env `YAMA_MCP_TOKEN` → `THIS_CFG.McpToken` → 皆空则随机生成(仅本次进程有效,打印到日志)。对话框内 token 必填、默认随机值(预填并随保存持久化到 `McpToken`)。
|
||||||
|
- ✅ **绑定地址**:默认 `127.0.0.1`(仅本机);可配置为 `0.0.0.0` 或内网 IP(如 `192.168.0.92`)以支持多人协作跨机调用。
|
||||||
|
- ✅ **配置入口**:主对话框「扩展」菜单,位于「地理信息(&L)」子菜单下方新增「MCP设置」菜单项,打开独立配置对话框。
|
||||||
|
- ✅ **生效方式**:MCP 启用/端口/绑定/token 的改动均需重启服务端生效,对话框保存后弹窗提醒「重启程序生效」。
|
||||||
|
- ✅ **取数方案**:C(抽公共函数 `BuildHostJson`,MCP 与 WebService 平级复用)。
|
||||||
|
- ✅ **返回字段**:全量,含实时信息(`rtt` / `activeWindow`),支持 AI 监督场景。
|
||||||
|
- ✅ **序列化**:`structuredContent` + `outputSchema`,`content` 附带可读摘要。
|
||||||
@@ -0,0 +1,486 @@
|
|||||||
|
# YAMA MCP 功能开发技术书(Phase 2 及后续)
|
||||||
|
|
||||||
|
> **状态**:设计定稿(已按两轮评审修订);P2a、P2b 已实现并经真实主机验证(`search_hosts` / `get_host_detail` / `list_processes` / `list_windows` / `get_activity_history`);P2c 已实现(`get_screenshot` / `list_files`);P2d 已实现并验证(`list_services` / `get_client_log`)。
|
||||||
|
> **读者**:MCP 后续功能的研发/评审人员。
|
||||||
|
> **关联文档**:[Mcp_Design.md](./Mcp_Design.md)(Phase 1 的协议、架构、配置与菜单设计)。
|
||||||
|
> **核心目标**:让 MCP 从「单个只读工具」平滑演进为「分阶段、可回滚、影响面可控」的工具集,**不一次性大改现有功能**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 背景与目标
|
||||||
|
|
||||||
|
Phase 1 已跑通 MCP 最小闭环:`httplib` + JSON-RPC 2.0,单工具 `list_online_hosts`,静态 Bearer token,经「扩展 → MCP设置」对话框开启。目标是把 YAMA 服务端已有的主机管理能力(进程、窗口、终端、截图、文件等)**逐步**暴露为 MCP 工具,供 Claude Code 等 AI 助手以工具调用方式使用。
|
||||||
|
|
||||||
|
本技术书解决三个问题:
|
||||||
|
|
||||||
|
1. **做什么**——工具路线图与优先级(只读 → 安全写 → 会话)。
|
||||||
|
2. **怎么做**——复用现有协议,无头驱动、不弹对话框;**区分「主连接 RPC」与「子链接流式」两种模式**。
|
||||||
|
3. **怎么稳**——每个阶段独立可交付、可回滚,影响面小、可验收。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 现状回顾(Phase 1 已交付)
|
||||||
|
|
||||||
|
| 模块 | 文件 | 职责 |
|
||||||
|
|------|------|------|
|
||||||
|
| HTTP/JSON-RPC 分发 | `McpServer.h` / `McpServer.cpp` | `POST /mcp`;token 校验;`initialize`/`ping`/`tools/list`/`tools/call` |
|
||||||
|
| 工具序列化复用 | `HostJson.h` / `HostJson.cpp` | `BuildHostJson(ctx, clientMap)`,单主机 JSON(与 WebService 共用) |
|
||||||
|
| 配置对话框 | `McpSettingsDlg.h` / `.cpp` | 运行时构造对话框:enable/port/bind/token |
|
||||||
|
| 生命周期接入 | `2015RemoteDlg.cpp:2136-2162`(启动)、`3900-3906`(停止) | 随主对话框启停 |
|
||||||
|
| FRP 暴露 | `2015RemoteDlg.cpp:3269-3304` | MCP 端口仅在「启用 + 绑 `0.0.0.0`」时纳入 FRP |
|
||||||
|
|
||||||
|
关键结论:**MCP 未引入任何新第三方库**——`httplib`(HTTP)与 `jsoncpp`(JSON)均为项目既有依赖。
|
||||||
|
|
||||||
|
当前工具实现只有一个 dispatch 分支(`McpServer.cpp` 的 `BuildToolsCall`),新增工具的边际成本很低:**注册 `outputSchema` + 加一个 `tools/call` 分支**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 设计原则(贯穿所有后续开发)
|
||||||
|
|
||||||
|
1. **只读优先,写操作单独评审**:延续 `Mcp_Design.md §1.3`。只读工具直接做;任何「改变被控端状态」的工具(命令执行、杀进程、传文件、控制)必须经安全评审 + 独立授权。
|
||||||
|
2. **复用现有协议,不新造命令**:一切数据获取/操控都走 `common/commands.h` 里既有的 `COMMAND_*`(服务端→客户端)与 `TOKEN_*`(客户端→服务端),不新增自定义命令。
|
||||||
|
3. **无头接管复用 Web 终端模式**:对话框只是 UI 外壳;MCP 复用 `WebService` 的挂起标记/接管模式,把数据接到 MCP 侧,**不弹框**。
|
||||||
|
4. **每阶段独立可交付、可回滚**:一个阶段 = 一批工具 + 对应验收;出错可单独 revert,不连坐。
|
||||||
|
5. **超时与清理是硬约束**:MCP 是 request/response,一次 `tools/call` 不能无限阻塞等数据;必须有超时、迟到回收、用完即关。
|
||||||
|
6. **输出 schema 先行**:每个工具先在 `tools/list` 里声明完整 `inputSchema`/`outputSchema`,AI 客户端据此决定何时调用、如何解析。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 核心机制:无头驱动(双模式)
|
||||||
|
|
||||||
|
> **本节是本技术书的关键**。此前设计把「进程/窗口/截图」误当作「子链接」,评审后纠正为:这些是**主连接 request/response**;真正走子链接的只有终端、完整屏幕、文件传输等大流量/持续流能力。两者实现机制不同,必须分开建模。
|
||||||
|
|
||||||
|
### 4.1 连接模型:主连接 vs 子链接
|
||||||
|
|
||||||
|
`common/commands.h` 定义了完整协议,分两层:
|
||||||
|
|
||||||
|
- **主连接**:客户端上线后与主控保持的长连接。承载心跳(`TOKEN_HEARTBEAT`)、命令下发,以及**轻量 request/response**——屏幕预览缩略图(`TOKEN_SCREEN_PREVIEW_RSP`)、历史活动(`TOKEN_REPORT_ACTIVITY`)等。响应以 `TOKEN_*` 回到主连接(捕获点见 §4.2)。
|
||||||
|
- **子链接**:屏幕、终端、文件传输、**进程/窗口列表**等功能,服务端下 `COMMAND_*` 后客户端**新建一条 TCP 子链接**回连,数据以 `TOKEN_*` 开头,作为一个**新的 `CONTEXT_OBJECT`** 进入分派。其中进程/窗口列表是**一次性子链接**(回一条 `TOKEN_PSLIST`/`TOKEN_WSLIST` 即关,见 §4.2 模式 A′),终端/屏幕/文件传输则是**持续流子链接**。
|
||||||
|
|
||||||
|
两者都由同一个 `MessageHandle(CONTEXT_OBJECT*)` 分派,靠 `ContextObject->GetClientID()` 关联到同一台主机。**判断一个 `TOKEN_*` 属于哪层,看它的数据落在哪个 context 上**:
|
||||||
|
|
||||||
|
| 证据 | 结论 |
|
||||||
|
|------|------|
|
||||||
|
| 客户端 `KernelManager.cpp:1156` `COMMAND_SYSTEM` → `new IOCPClient` 子链接 + `LoopProcessManager`;`SystemManager.cpp:75` `GetProcessList()` 里 `szBuffer[0]=TOKEN_PSLIST`,经子链接 `Send2Server` 回传 | 进程/窗口列表 = **一次性子链接**(主连接下 `COMMAND_SYSTEM`/`COMMAND_WSLIST`,子连接回 `TOKEN_PSLIST`/`TOKEN_WSLIST`) |
|
||||||
|
| `2015RemoteDlg.cpp:8527-8536`:`SendScreenPreviewRequest` 用 `ctx->Send2Client(...)`;响应 `TOKEN_SCREEN_PREVIEW_RSP` 在 `MessageHandle:6173` | 屏幕预览 = **主连接 RPC** |
|
||||||
|
| `WebService.cpp:1836-1900` + `2015RemoteDlg.cpp:6312-6351`:`COMMAND_SHELL` → 客户端建 shell 子上下文 → `TOKEN_SHELL_START`/`TOKEN_TERMINAL_START` | 终端 = **子链接** |
|
||||||
|
|
||||||
|
### 4.2 模式 A:主连接 RPC(截图 / 历史活动)
|
||||||
|
|
||||||
|
适用于:`get_screenshot`(屏幕预览)、`get_activity_history`(历史活动)。
|
||||||
|
|
||||||
|
```
|
||||||
|
MCP 工具 tools/call
|
||||||
|
└─► 主 context ctx = FindHost(device_id)
|
||||||
|
└─► ctx->Send2Client(COMMAND_*) // 主连接发 1 字节命令
|
||||||
|
客户端处理后在主连接回 TOKEN_*
|
||||||
|
└─► MessageHandle 收到 TOKEN_*(数据已在 ctx->InDeCompressedBuffer)
|
||||||
|
├─ if (McpServer().IsPending(devId))
|
||||||
|
│ McpServer().TakeMainResponse(devId, ctx->GetBuffer(0), ctx->GetBufferLength()); // IO 线程同步拷贝
|
||||||
|
│ break; // 跳过 MFC 弹框(不 CancelIO,主连接不可关)
|
||||||
|
└─ 否则走原 MFC 路径(如打开历史活动对话框)
|
||||||
|
```
|
||||||
|
|
||||||
|
要点:
|
||||||
|
|
||||||
|
- **无子链接、无 subCtx**:数据就在主 context 缓冲里,MCP 只需在 `MessageHandle` 的对应 `TOKEN_*` 分支里**同步拷贝**到结果缓冲,再唤醒等待者。
|
||||||
|
- **不 CancelIO**:响应落在主连接上,关闭会断开整个主机会话;故拦截后仅 `TakeMainResponse` + `break`,主连接保持。
|
||||||
|
- **捕获点因工具而异(必须在 IO 线程同步拷贝)**:屏幕预览的 `TOKEN_SCREEN_PREVIEW_RSP` **直接读本消息的 `szBuffer`/`len`**(`2015RemoteDlg.cpp:6173-6185` 已在 case 内把 `szBuffer` 拷贝到堆消息再转主线程);历史活动的 `TOKEN_REPORT_ACTIVITY` 读 `GetBuffer(0)`(= `InDeCompressedBuffer`)。两者都是每 context 单消息缓冲,主连接 recv 循环处理完本条消息后即被覆盖,故拷贝必须发生在 `MessageHandle` 内部(IO 线程),不能异步等 UI 线程。
|
||||||
|
- **请求关联**:`get_screenshot` 有 `reqId`(`ScreenPreviewReq.reqId`)可丢弃过期响应;`get_activity_history` **无请求 id**,靠「每 host 单飞行」(见 §4.4)保证正确性。
|
||||||
|
|
||||||
|
### 4.2′ 模式 A′:一次性子链接(进程 / 窗口列表)
|
||||||
|
|
||||||
|
适用于:`list_processes`、`list_windows`。**这是本技术书相对原设计的重要纠正**:进程/窗口列表**不是**主连接 RPC,而是一次性子链接——`COMMAND_PSLIST` 在主连接上是空操作(客户端 `KernelManager::OnReceive` 无此 case),正确的触发命令是 `COMMAND_SYSTEM`(进程)/`COMMAND_WSLIST`(窗口)。
|
||||||
|
|
||||||
|
```
|
||||||
|
MCP 工具 tools/call
|
||||||
|
└─► 主 context ctx = FindHost(device_id)
|
||||||
|
└─► ctx->Send2Client(COMMAND_SYSTEM / COMMAND_WSLIST) // 主连接发 1 字节命令
|
||||||
|
客户端 KernelManager::OnReceive 收到后新建一条子链接(new IOCPClient,EnableSubConnAuth)
|
||||||
|
└─► 子链接 ConnectServer 后先发 TOKEN_CONN_AUTH(服务端 SetID 把 clientID 钉在子 context 上)
|
||||||
|
└─► CSystemManager 构造函数立即 Send2Server(TOKEN_PSLIST / TOKEN_WSLIST)
|
||||||
|
└─► MessageHandle 在【子 context】上收到 TOKEN_PSLIST / TOKEN_WSLIST(GetClientID()==主 host id)
|
||||||
|
├─ if (McpServer().IsPending(devId))
|
||||||
|
│ McpServer().TakeMainResponse(devId, GetBuffer(0), GetBufferLength());
|
||||||
|
│ ContextObject->CancelIO(); // 用完即关一次性子链接(不关会泄漏)
|
||||||
|
│ break; // 跳过 WM_OPENSYSTEMDIALOG
|
||||||
|
└─ 否则 g_2015RemoteDlg->SendMessage(WM_OPENSYSTEMDIALOG, ...) // MFC 路径
|
||||||
|
```
|
||||||
|
|
||||||
|
要点:
|
||||||
|
|
||||||
|
- **有子链接、有 subCtx**:数据落在子 context 上,非主 context。子 context 的 `GetClientID()` 已由 `TOKEN_CONN_AUTH`(`2015RemoteDlg.cpp:6291` 的 `SetID`)钉成主连接 clientID,故 `IsPending(devId)` 能与主 context 的 `device_id` 对上。
|
||||||
|
- **用完即关**:MCP 拦截后 `CancelIO()` 关闭子链接(客户端 IOCPClient 以 `exit_while_disconnect=true` 构造,会随断开退出);不关会泄漏子链接。
|
||||||
|
- **与 MFC 对话框可并存**:MCP 与 MFC 对话框**各自使用独立的子链接**(客户端每次 `COMMAND_SYSTEM`/`COMMAND_WSLIST` 都新建一条),因此两者互不阻塞——MCP 的一次性调用不会导致 MFC 无法查看进程/窗口,反之亦然。无请求 id 的串扰靠「每 host 单飞行」(§4.4)规避,而非「与 MFC 对话框互斥」。
|
||||||
|
- **编码**:进程名/路径按 clientType 判定(Windows=GBK/936、LNX/MAC=UTF-8,进程枚举走 A 接口不随 `CLIENT_CAP_UTF8` 转 UTF-8);窗口标题为客户端 UTF-8(按能力位 `CLIENT_CAP_UTF8` 判别,老客户端回落 CP936)。
|
||||||
|
- **请求关联**:无请求 id → 每 host 单飞行(§4.4)。
|
||||||
|
|
||||||
|
- **文件列举也走 A′(`list_files`)**:列盘与进程/窗口同构(`COMMAND_LIST_DRIVE`→一次性子链接→`TOKEN_DRIVE_LIST` 即关);列目录则在此一次性子链接上**多一轮** `COMMAND_LIST_FILES`→`TOKEN_FILE_LIST` 后再关。故 `list_files` 归入模式 A′,而非原设计的模式 A(详见 §6.3)。
|
||||||
|
|
||||||
|
> **模式 A0(主连接单向命令,无响应)**:`kill_process`(`COMMAND_KILLPROCESS`)、`send_message`(`COMMAND_TALK`)这类「发完即走」的命令没有 MCP 需要的响应数据,**不需要** §4.4 的挂起注册表/超时/单飞行——`Send2Client` 后直接返回成功。它是模式 A 的退化情形,实现最简单。
|
||||||
|
|
||||||
|
### 4.3 模式 B:子链接流式(终端 / 完整屏幕 / 完整文件)
|
||||||
|
|
||||||
|
适用于:`exec_command`(终端),以及未来的实时远程桌面、完整文件传输。
|
||||||
|
|
||||||
|
```
|
||||||
|
MCP 工具 tools/call
|
||||||
|
└─► 登记挂起请求 m_Pending[device_id](标记 mode=B)
|
||||||
|
└─► ctx = FindHost(device_id); ctx->Send2Client(COMMAND_SHELL) // 主连接下命令
|
||||||
|
客户端建 shell 子链接
|
||||||
|
└─► MessageHandle 收到 TOKEN_SHELL_START / TOKEN_TERMINAL_START
|
||||||
|
├─ if (McpServer().IsPending(devId))
|
||||||
|
│ McpServer().TakeSubContext(devId, ContextObject); // 接管子上下文
|
||||||
|
│ break; // 跳过弹框
|
||||||
|
└─ 否则弹 WM_OPENSHELLDIALOG
|
||||||
|
后续 shell 输出
|
||||||
|
└─► MessageHandle 顶部 IsTerminalContext 分支 → McpServer().OnShellData(...) 泵数据
|
||||||
|
```
|
||||||
|
|
||||||
|
完全复刻 `WebService` 的 `IsTermPending`/`RegisterTerminalContext`/`OnTerminalData` 模式(`WebService.h:283-287`),但**不复用其长会话状态**——MCP 是「一次性:建链 → 收发 → 关链」。
|
||||||
|
|
||||||
|
### 4.4 统一挂起请求注册表
|
||||||
|
|
||||||
|
模式 A(主连接 RPC)与模式 A′(一次性子链接)共用一个按 `device_id`(= `context::GetClientID()`)索引的注册表。**当前实现(P2b)只覆盖 A/A′ 两种「一次性响应」**,故结构里只有 `data` + `done`,无需 `mode`/`subCtx`/`timedOut`;未来 P3a 的 `exec_command`(模式 B 流式)再扩展 `subCtx` 字段。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
struct PendingRequest {
|
||||||
|
std::string tool;
|
||||||
|
std::vector<BYTE> data; // 结果缓冲(MessageHandle 内同步拷贝的字节)
|
||||||
|
bool done = false;
|
||||||
|
};
|
||||||
|
std::mutex m_PendingMutex;
|
||||||
|
std::condition_variable m_PendingCv;
|
||||||
|
std::map<uint64_t, PendingRequest> m_Pending; // 受 m_PendingMutex 保护
|
||||||
|
```
|
||||||
|
|
||||||
|
暴露给 `MessageHandle` 的钩子:
|
||||||
|
|
||||||
|
- `bool IsPending(uint64_t device_id)` —— 该 `TOKEN_*` 是否为 MCP 触发。
|
||||||
|
- `void TakeMainResponse(uint64_t device_id, const BYTE* data, ULONG len)` —— 模式 A/A′:拷贝缓冲、`done=true`、唤醒 `cv`(在 IO 线程阻塞期间同步调用,拷贝安全)。
|
||||||
|
|
||||||
|
工具线程侧的方法:
|
||||||
|
|
||||||
|
- `bool BeginPending(uint64_t device_id, const std::string& tool)` —— 登记挂起(`false` = 该 host 已有挂起,返回「设备忙」)。
|
||||||
|
- `bool WaitPending(uint64_t device_id, std::vector<BYTE>& out, int timeoutMs)` —— 等待响应(超时/失败自动清理并返回 `false`)。
|
||||||
|
- `void ClearPending(uint64_t device_id)` —— 提前退出路径的清理。
|
||||||
|
|
||||||
|
**每 host 单飞行**:同一 `device_id` 同一时刻只允许一个挂起请求。这是「无请求 id」工具的正确性前提——进程/窗口/历史活动响应都不带请求 id,若同 host 并发两个请求,无法区分响应归属,故并发第二个请求返回「设备忙」(`-32003`)而非串扰。
|
||||||
|
|
||||||
|
> 说明:单飞行只约束「MCP 侧同 host 并发」,**不**约束「MCP 与 MFC 对话框并存」——二者各用独立子链接(模式 A′)或各自触发(模式 A),互不占位。详见 §4.2′。
|
||||||
|
|
||||||
|
### 4.5 超时与清理(MCP 与 Web 的关键差异)
|
||||||
|
|
||||||
|
Web 是长连接,子链接可长驻;MCP 是**一次性 request/response**。因此:
|
||||||
|
|
||||||
|
- **超时**:`wait_for(timeout)` 等数据;当前 P2b 用编译期常量 `kMcpToolTimeoutMs = 20000`(暂未接配置,见 §9)。超时返回 JSON-RPC 错误(`-32001`),并清理注册表。
|
||||||
|
- **迟到回收(已知边界)**:数据在超时后才到达(`IsPending` 已清)时,拦截分支不成立,响应会**回落到原 MFC 路径**(进程/窗口弹 `WM_OPENSYSTEMDIALOG`,历史活动弹对话框)——属「客户端在 20s 内未响应」这一病态场景的罕见副作用,不会串扰或崩溃。正常路径(<1s 内响应)不受影响。若未来要彻底消除,可给注册表加「超时墓碑 + 宽限期丢弃」。
|
||||||
|
- **用完即关**:模式 A′(进程/窗口)拦截后 `CancelIO()` 关一次性子链接;模式 A(主连接 RPC)不关主连接。未来模式 B(终端)收集完 stdout 后主动关子上下文。
|
||||||
|
- **阻塞 httplib worker**:`wait_for` 会阻塞 `HandleMcp` 所在的 httplib worker 线程。httplib 默认线程池约 8 线程,阻塞一个 20–30s 会占住一个 worker。MCP 属低频串行调用(AI 逐条调用),**本阶段可接受**;但需在 §12 显式声明,若未来需并发改用「提交任务 + 轮询结果」的异步模型。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 工具路线图(分阶段)
|
||||||
|
|
||||||
|
### 5.1 工具总表
|
||||||
|
|
||||||
|
> 「模式」标注实现路径(A=主连接 RPC,A′=一次性子链接,A0=主连接单向命令[无响应],B=子链接流式,—=纯内存)。
|
||||||
|
|
||||||
|
| 阶段 | 工具 | 功能 | 复用协议/函数 | 模式 | 风险 |
|
||||||
|
|------|------|------|---------------|------|------|
|
||||||
|
| P1(已交付) | `list_online_hosts` | 在线主机列表 | `BuildHostJson` | — | 只读 |
|
||||||
|
| P2a(已实现并验证) | `search_hosts` | 按名称/IP/分组/OS/在线态过滤 | `list_online_hosts` 内存过滤 | — | 只读 |
|
||||||
|
| P2a(已实现并验证) | `get_host_detail` | 单机详情(不含活动历史) | 现成 device 字段(`BuildHostJson`) | — | 只读 |
|
||||||
|
| P2b(已实现并验证) | `list_processes` | 主机进程列表 | `COMMAND_SYSTEM`→(子链接)`TOKEN_PSLIST` | **A′** | 只读 |
|
||||||
|
| P2b(已实现并验证) | `list_windows` | 主机窗口列表 | `COMMAND_WSLIST`→(子链接)`TOKEN_WSLIST` | **A′** | 只读 |
|
||||||
|
| P2b(已实现并验证) | `get_activity_history` | 单机历史活动(文本快照) | `COMMAND_QUERY_ACTIVITY`(159)→`TOKEN_REPORT_ACTIVITY`(160) | **A** | 只读 |
|
||||||
|
| P2c(已实现) | `get_screenshot` | 单帧屏幕截图(JPEG) | `COMMAND_SCREEN_PREVIEW_REQ`→`TOKEN_SCREEN_PREVIEW_RSP` | **A** | 只读 |
|
||||||
|
| P2c(已实现) | `list_files` | 目录列举 | `COMMAND_LIST_DRIVE`→`TOKEN_DRIVE_LIST`(列盘)/ 同子链接再 `COMMAND_LIST_FILES`→`TOKEN_FILE_LIST`(列目录) | **A′** | 只读 |
|
||||||
|
| P2d(已实现) | `list_services` | Windows 服务列表 | `COMMAND_SERVICES`(218)→(子链接)`TOKEN_SERVERLIST` | **A′** | 只读 |
|
||||||
|
| P2d(已实现) | `get_client_log` | 客户端运行日志快照 | `COMMAND_QUERY_LOG`(155)→(子链接)`TOKEN_REPORT_LOG`(156) | **A′** | 只读 |
|
||||||
|
| P3 | `exec_command` | 执行命令、返回 stdout | `COMMAND_SHELL`→`TOKEN_SHELL_START`/`TOKEN_SHELL_DATA` | **B** | **写** |
|
||||||
|
| P3 | `send_message` | 给被控端弹消息 | `COMMAND_TALK`(220) | **A0** | 写(低危) |
|
||||||
|
| P3 | `kill_process` | 结束进程 | `COMMAND_KILLPROCESS` | **A0** | **写** |
|
||||||
|
| P4(暂缓) | 实时远程桌面 / 交互终端 | 持续流 | 屏幕/终端子链接 + SSE/轮询 | **B** | 需会话模型 |
|
||||||
|
|
||||||
|
### 5.2 Phase 2:只读观测(低风险,无写操作)
|
||||||
|
|
||||||
|
全部只读,不改变被控端任何状态,token 模型不变(仍单一静态 token)。P2a 纯内存、零改动;P2b/P2c/P2d 引入模式 A/A′ 无头接管,但只读、可回滚。
|
||||||
|
|
||||||
|
### 5.3 Phase 3:安全写操作(需单独评审)
|
||||||
|
|
||||||
|
进入 `Mcp_Design.md §1.3` 明确划出的「写操作」区。每个工具交付前需评审三点(见 §8):命令白名单、审计日志、**分组授权 + token 分级**。
|
||||||
|
|
||||||
|
### 5.4 Phase 4:会话/流式(暂缓)
|
||||||
|
|
||||||
|
实时远程桌面、交互终端等需要「持续流」的能力,与当前 request/response 模型不匹配。需先决定传输方案(SSE / 提交任务 + 轮询),本期**不做**,避免过早引入复杂会话状态。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 分阶段实施计划
|
||||||
|
|
||||||
|
> 每期都给出:改动文件、接口、测试点、回滚方式。原则是**每期可独立合入、独立验收、独立回滚**。
|
||||||
|
|
||||||
|
### 6.1 P2a:`search_hosts` + `get_host_detail`(纯内存,零子链接)✅ 已实现并验证
|
||||||
|
|
||||||
|
**改动**:仅 `McpServer.cpp`。
|
||||||
|
- `BuildToolsListResult` 增两个工具描述 + schema。
|
||||||
|
- `BuildToolsCall` 增两个分支,复用 `list_online_hosts` 已遍历出的主机数据做内存过滤/单机抽取(抽出 `CollectOnlineHosts` 收集 + `BuildHostItemSchema` 复用)。
|
||||||
|
- 无任何 `MessageHandle`/`WebService`/`context` 改动。
|
||||||
|
|
||||||
|
**影响面**:极小,只加不改。**回滚**:revert 单文件即可。
|
||||||
|
|
||||||
|
**验收**:`tools/list` 返回 3 个工具;`tools/call search_hosts` 按 filter 正确过滤;`get_host_detail` 对在线主机返回正确字段、对离线/非法 id 返回明确错误。
|
||||||
|
|
||||||
|
> **范围修正**:原计划 `get_host_detail` 附带「活动历史」,但活动历史是 `COMMAND_QUERY_ACTIVITY`(159)→`TOKEN_REPORT_ACTIVITY`(160) 的**主连接往返**(客户端上报、服务端不落库),非纯内存字段,违反 P2a「纯内存」约束。故 P2a 的 `get_host_detail` 只返回主机实时字段,活动历史单列为 P2b 的 `get_activity_history`(模式 A)。
|
||||||
|
|
||||||
|
### 6.2 P2b:`list_processes` + `list_windows` + `get_activity_history`(模式 A′ / 模式 A,首次无头接管)✅ 已实现并验证
|
||||||
|
|
||||||
|
> 本节已按实际实现修订:进程/窗口是**一次性子链接**(模式 A′),历史活动是**主连接 RPC**(模式 A),见 §4.2/§4.2′。
|
||||||
|
|
||||||
|
**改动**:
|
||||||
|
|
||||||
|
- `McpServer.h/.cpp`:
|
||||||
|
- 加 §4.4 的 `m_Pending` 注册表 + `IsPending`/`TakeMainResponse`/`BeginPending`/`WaitPending`/`ClearPending`,每 host 单飞行检查。
|
||||||
|
- 加解析器 `ParseProcessList`(PID/name/arch/path,按 clientType 编码转 UTF-8)、`ParseWindowList`(hwnd/title/status/pid,标题按能力位解码);两者都用 `BoundedStrlen` 有界读 + 「空记录 = 尾部零填充」终止,规避客户端 `LocalSize` 对齐引入的尾部零字节。
|
||||||
|
- 加三个工具:`list_processes`(发 `COMMAND_SYSTEM`)、`list_windows`(发 `COMMAND_WSLIST`)、`get_activity_history`(发 `COMMAND_QUERY_ACTIVITY`)。
|
||||||
|
- `2015RemoteDlg.cpp` 的 `MessageHandle`:
|
||||||
|
- `TOKEN_PSLIST`/`TOKEN_WSLIST` 分支(子 context):`if (McpServer().IsPending(devId)) { TakeMainResponse(devId, GetBuffer(0), GetBufferLength()); ContextObject->CancelIO(); break; }`(**在 UI 线程、IO 线程阻塞期间同步拷贝**;用完即关子链接)。
|
||||||
|
- `TOKEN_REPORT_ACTIVITY` 分支(主 context):`if (McpServer().IsPending(devId)) { TakeMainResponse(...); break; }`(**不 CancelIO**,主连接不可关)。
|
||||||
|
|
||||||
|
**影响面**:`MessageHandle` 只新增 `if` 分支,不影响现有 MFC 弹框路径(非 MCP 触发时 `IsPending` 为假,行为不变)。
|
||||||
|
|
||||||
|
**验收**:
|
||||||
|
- MFC 双击进程/窗口管理弹框仍正常(回归点);MFC 历史活动对话框仍正常(回归点)。
|
||||||
|
- MCP `list_processes` 返回 `[{pid,name,arch,path}]`;`list_windows` 返回 `[{hwnd,title,status,pid}]`;`get_activity_history` 返回 `{records:[...], activityHistory}`(UTF-8 文本,逐行拆记录)。
|
||||||
|
- 超时(如对离线主机)返回 `-32001` 而非挂死;同 host 并发第二次调用返回 `-32003`「设备忙」;发送失败返回 `-32004`。
|
||||||
|
- 中文进程名/窗口标题无乱码(进程名按 clientType 解码、窗口标题按能力位 UTF-8)。
|
||||||
|
|
||||||
|
**验证记录**(2026-08-19,真实在线主机实测):
|
||||||
|
- `list_processes`(Windows 11 主机):`pid`/`name`/`arch`/`path` 完整;`pid==0` 仅 1 条(合法 `[System Process]`),无尾部零填充空记录——padding 防护生效。
|
||||||
|
- `list_windows`:`hwnd`/`title`/`status`/`pid` 完整,无空标题记录;中文标题(「搭建 MCP 架构…」「微信」)UTF-8 正确。
|
||||||
|
- `get_activity_history`:返回 `[时间] [标题] 时长` 记录,UTF-8 正确。
|
||||||
|
- 错误路径实测:缺 `id` → `-32602`;非数字 `id` → `-32602`;未知/离线主机 → `-32002`;错误 token → HTTP 401。
|
||||||
|
- **客户端版本门槛**:`get_activity_history` 依赖客户端 `2026-08-15`(提交 `29929e4`)加入的 `COMMAND_QUERY_ACTIVITY` 处理;更早的客户端(如 `Jul 14 2026`)不识别该命令、无响应,服务端 20s 超时返回 `-32001`——属预期行为,非 bug。测试须选 `Aug 15 2026` 及之后的客户端。
|
||||||
|
|
||||||
|
**回滚**:revert `McpServer.h` + `McpServer.cpp` + `MessageHandle` 两处 `if` 分支。
|
||||||
|
|
||||||
|
### 6.3 P2c:`get_screenshot` + `list_files`(模式 A / A′)✅ 已实现(待实机验证)
|
||||||
|
|
||||||
|
> 本节按实际实现修订:`get_screenshot` 是**主连接 RPC**(模式 A,有 reqId 关联);`list_files` 是**一次性子链接**(模式 A′)——列盘 `COMMAND_LIST_DRIVE`→`TOKEN_DRIVE_LIST` 即关,列目录则在 `TOKEN_DRIVE_LIST` 到达时就地同子链接再发 `COMMAND_LIST_FILES`→`TOKEN_FILE_LIST` 后关链。原设计把 `list_files` 误标为模式 A,已纠正(与 §4.2′ 对进程/窗口的同类纠正一致)。
|
||||||
|
|
||||||
|
**`get_screenshot`(屏幕预览链路,主连接 RPC,模式 A)**:
|
||||||
|
|
||||||
|
- 复用现有双击预览机制 `COMMAND_SCREEN_PREVIEW_REQ`(247)→`TOKEN_SCREEN_PREVIEW_RSP`(248)(`common/commands.h:416-455`,单帧 JPEG,`ScreenPreviewReq`/`ScreenPreviewRspHeader`),`ctx->Send2Client` 发请求、主连接回响应,**不建子链接、不弹框**。
|
||||||
|
- 复用 MFC 预览的 RTT/FRP 自适应参数挑选 `ChooseScreenPreviewParams` + `SendScreenPreviewRequest`,不重复实现 `GetTargetQualityLevel`。
|
||||||
|
- **可选 `max_width` 参数**:缺省/0 沿用缩略图档位(最大 1024,4K 源屏高档 1280);`max_width`>0 覆盖宽度并钳制到客户端上限 `[64,1920]`(客户端 `CaptureAndEncodePreview` 硬钳制),供 AI 视觉/OCR 场景请求接近原分辨率(1080p 源屏传 1920 即原分辨率,4K 源屏最多 1920)。`jpegQuality` 仍沿用档位自适应值。
|
||||||
|
- **请求关联**:`ScreenPreviewReq.reqId` 提供关联;MCP 用独立 16 位 reqId 发生器(`NextPreviewReqId`,跳过 0)+ `SetPendingReqId` 登记期望值,`TakePreviewResponse` 只接受「挂起命中 + reqId 一致」的响应,过期/他途(MFC 预览并发)响应原样回落到 MFC 路径。
|
||||||
|
- 客户端能力位 `CLIENT_CAP_SCREEN_PREVIEW`(0x0004,仅 Windows 客户端声明):非 Windows 主机返回 `-32005`「不支持」。
|
||||||
|
- 返回结构:`content[0] = {type:"image", data: base64(JPEG), mimeType:"image/jpeg"}` + `structuredContent.image = {mimeType,width,height,bytes}`(元数据,不含 base64,避免二次膨胀)。
|
||||||
|
|
||||||
|
**`list_files`(文件链路,一次性子链接,模式 A′)**:
|
||||||
|
|
||||||
|
- 复用经典文件链路 `COMMAND_LIST_DRIVE`→`TOKEN_DRIVE_LIST` 与 `COMMAND_LIST_FILES`→`TOKEN_FILE_LIST`。**明确范围**:本阶段只做经典链路;项目另有 `COMMAND_GET_FOLDER`(66)、插件版 `TOKEN_DRIVE_LIST_PLUGIN`(150)、V2 文件传输,均列为非目标。
|
||||||
|
- **两种调用形态**:
|
||||||
|
- `path` 缺省/空(或 `.`/`/`/`\`)→ 只列盘:`COMMAND_LIST_DRIVE` 开子链接,`TOKEN_DRIVE_LIST` 到达即取走盘列表并关链(与进程/窗口同构的 A′)。
|
||||||
|
- `path` 指定目录 → 列目录:`TOKEN_DRIVE_LIST` 到达时 `OnDriveList` 就地同子链接下发 `COMMAND_LIST_FILES`+`path`(`BuildListFiles` 已按客户端 ANSI 转好,`OnDriveList` 不再转),等 `TOKEN_FILE_LIST` 取走并关链。
|
||||||
|
- **编码**:盘符类型名/文件系统、文件名、`path` 均按 **clientType** 判定(与进程名/路径一致):Windows 走 A 接口(`FindFirstFileA`/`GetLogicalDriveStringsA`/`SHGetFileInfoA` 等)= 客户端 ANSI(GBK/936);LNX/MAC 文件系统天然 UTF-8。**不能用 `GetClientEncoding`**——它按能力位返回 CP_UTF8,会误解声明 `CLIENT_CAP_UTF8` 的 Windows 客户端(其文件名实为 GBK)。解析用 `ToUtf8(buf, cp)`、下发用 `ToAnsi(path, cp)`,`cp = (clientType=="LNX"||"MAC") ? CP_UTF8 : 936`。
|
||||||
|
- **大列表保护**:`ParseFileList(data, 500, cp)` 条目上限 500,避免 JSON 响应过大。
|
||||||
|
- 返回结构:列盘 → `structuredContent.drives[]`;列目录 → `structuredContent.files[]`(`{name,isDir,size,mtime}`,mtime 为 Unix 秒)。
|
||||||
|
|
||||||
|
**改动**:
|
||||||
|
|
||||||
|
- `McpServer.h/.cpp`:
|
||||||
|
- 挂起注册表扩展:`PendingRequest` 增 `path`、`expectedReqId` 字段;`BeginPending(devId, tool, path)` 重载,新增 `NextPreviewReqId`/`SetPendingReqId`/`TakePreviewResponse`/`OnDriveList` 四方法。
|
||||||
|
- 解析器:`ParseDriveList`(`[letter][type][totalMB:4][freeMB:4][typeName\0][fileSystem\0]`,`letter=='\0'` 终止)、`ParseFileList`(`[attr][filename\0][sizeHigh:4][sizeLow:4][ft:8]`,空记录/上限 500 终止)。
|
||||||
|
- 工具:`get_screenshot`(模式 A)、`list_files`(模式 A′),注册 schema(`get_screenshot` 无 inputSchema、`list_files` 带可选 `path`)。
|
||||||
|
- `2015RemoteDlg.cpp` 的 `MessageHandle`(三处拦截):
|
||||||
|
- `TOKEN_SCREEN_PREVIEW_RSP`(主 context):`TakePreviewResponse(devId, reqId, szBuffer, len)` 命中则 break(**不 CancelIO**,主连接不可关)。
|
||||||
|
- `TOKEN_DRIVE_LIST`(子 context):`if (IsPending) { OnDriveList(...) ? CancelIO() : /*继续等*/ break; }`(列盘关链、列目录不关)。
|
||||||
|
- `TOKEN_FILE_LIST`(子 context,**新增 case**):`if (IsPending) TakeMainResponse(...); CancelIO();`(无论命中都关孤儿子链接,防泄漏)。
|
||||||
|
|
||||||
|
**影响面**:`MessageHandle` 仅新增 `if` 分支与一个 `TOKEN_FILE_LIST` case;非 MCP 触发时 `IsPending` 为假、`TOKEN_FILE_LIST` case 在 MFC 路径下因 `hDlg` 接管根本不进 `MessageHandle`,现有 MFC 文件管理/屏幕预览弹框路径不变。
|
||||||
|
|
||||||
|
**验收 / 实机验证点(待构建后执行)**:
|
||||||
|
- MFC 双击文件管理弹框仍正常(回归点);MFC 双击预览缩略图仍正常(回归点)。
|
||||||
|
- MCP `get_screenshot` 返回 JPEG(base64 + 元数据),Windows 主机正常;非 Windows 主机(或未声明 `CLIENT_CAP_SCREEN_PREVIEW`)返回 `-32005`。
|
||||||
|
- MCP `list_files`(无 `path`)返回 `drives[]`;`list_files(path="C:\\")` 返回 `files[]`(含目录/文件/大小/mtime),中文文件名无乱码;条目 >500 时截断为 500。
|
||||||
|
- 错误路径:缺 `id`/非数字 `id` → `-32602`;未知/离线主机 → `-32002`;同 host 并发 → `-32003`;发送失败 → `-32004`;超时 → `-32001`。
|
||||||
|
|
||||||
|
**回滚**:revert `McpServer.h` + `McpServer.cpp` + `MessageHandle` 三处拦截。
|
||||||
|
|
||||||
|
### 6.4 P2d:`list_services` + `get_client_log`(模式 A′,只读感知扩展)✅ 已实现并验证
|
||||||
|
|
||||||
|
> 本期新增两个**只读**感知工具,均走**一次性子链接**(模式 A′),复用 §4.4 挂起注册表,无新增机制、无写操作。二者与 P2b 的进程/窗口、P2c 的列盘同构:主连接下发命令 → 客户端建子链接回传 → 取走即 `CancelIO`。
|
||||||
|
|
||||||
|
**`list_services`(服务管理链路,一次性子链接,仅 Windows)**:
|
||||||
|
|
||||||
|
- 复用经典服务链路 `COMMAND_SERVICES`(218)→`TOKEN_SERVERLIST`。客户端 `KernelManager::OnReceive` 收到 `COMMAND_SERVICES` 建子链接 `LoopServicesManager`,`CServicesManager` **构造函数即** `SendServicesList()`(无需再下 `COMMAND_SERVICELIST` 刷新命令),在子链接上一次性回传 `TOKEN_SERVERLIST`。
|
||||||
|
- **记录格式**(`ServicesManager.cpp::GetServicesList`):`[TOKEN_SERVERLIST:1]` 后接 N 条 `[displayName\0][serviceName\0][binaryPath\0][runWay\0][autoRun\0]`,尾部 `LocalAlloc(LPTR)` 零填充终止。**字段顺序以代码为准**(`displayName` 在前、`serviceName` 在后;源码注释「真实名称/显示名称」标注相反,勿据注释判断)。`runWay`/`autoRun` 为英文枚举串(`Stopped/Running/Paused/…`、`Boot-Start/Auto-Start/Demand-Start/Disabled/…`)。
|
||||||
|
- **编码**:字段全部来自 Windows A 接口(`EnumServicesStatus`/`QueryServiceConfig` 返回 ANSI),按 clientType 判定 `cp`(与进程/文件一致);仅 Windows 客户端实现(`EnumServicesStatus` 为 Windows-only),LNX/MAC 主机提前返回 `-32005`。
|
||||||
|
- 返回结构:`structuredContent.services[] = {display_name, service_name, binary_path, status, start_type}`。
|
||||||
|
|
||||||
|
**`get_client_log`(客户端运行日志,一次性子链接取首条全量)**:
|
||||||
|
|
||||||
|
- 复用经典日志链路 `COMMAND_QUERY_LOG`(155)→`TOKEN_REPORT_LOG`(156)。客户端 `KernelManager::OnReceive` 收到 `COMMAND_QUERY_LOG` 建子链接 `LoopClientLogManager`,`CClientLogManager` 构造函数 `SendLogDump()`(`m_sentIdx=0` 返回**全量**),并启动 push 线程**每 3s 推增量**。
|
||||||
|
- **关键语义**:`TOKEN_REPORT_LOG` 会**多次**到达(全量 + 每 3s 增量)。MCP 只取**首条全量**:`MessageHandle` 在 `IsPending` 命中时 `TakeMainResponse` 后立即 `CancelIO` 关子链接,后续增量不再到达(与 MFC 日志对话框「持续接收增量、不关链」的路径区分)。
|
||||||
|
- **日志内容**:YAMA 客户端**自身运行时日志**(`Logger` 内存 ring buffer,最近最多 1000 条,含 `[pid][时间戳] [文件:行号] 消息`),**非** Windows 系统日志。
|
||||||
|
- **编码**:日志文本为客户端 ANSI(Windows 走 `vsnprintf` A 版),按 clientType 判定 `cp`;`data[0]=token` 后为无 `'\0'` 终止的原始字节,需先 `std::string` 包装再 `ToUtf8`(`ToUtf8` 内部以 `-1` 读 null 终止串)。
|
||||||
|
- 返回结构:`structuredContent.log = "<原始日志文本>"`。
|
||||||
|
|
||||||
|
**改动**:
|
||||||
|
|
||||||
|
- `McpServer.h/.cpp`:
|
||||||
|
- 解析器 `ParseServiceList`(5 字段记录,有界读 + 「displayName 与 serviceName 皆空 = 尾部零填充」终止)。
|
||||||
|
- schema `BuildServiceListOutputSchema` / `BuildClientLogOutputSchema`(inputSchema 复用 `BuildGetHostDetailInputSchema` 的 `{ id }`)。
|
||||||
|
- 工具 `BuildListServices`(发 `COMMAND_SERVICES`,LNX/MAC 提前 `-32005`)、`BuildGetClientLog`(发 `COMMAND_QUERY_LOG`)。
|
||||||
|
- `2015RemoteDlg.cpp` 的 `MessageHandle`(两处拦截):
|
||||||
|
- `TOKEN_SERVERLIST`(子 context):`if (IsPending) { TakeMainResponse; CancelIO(); break; }`(一次性,取走即关)。
|
||||||
|
- `TOKEN_REPORT_LOG`(子 context):`if (IsPending) { TakeMainResponse; CancelIO(); break; }`(取首条全量即关,停止 3s 增量)。
|
||||||
|
|
||||||
|
**影响面**:`MessageHandle` 仅新增两个 `if` 分支;非 MCP 触发时 `IsPending` 为假,MFC 服务管理弹框与日志对话框路径不变(回归点)。
|
||||||
|
|
||||||
|
**验收 / 实机验证点**:
|
||||||
|
- MFC 双击「服务管理」弹框仍正常(回归点);MFC 打开「客户端运行日志」对话框仍正常、增量持续追加(回归点)。
|
||||||
|
- MCP `list_services` 对 Windows 主机返回 `services[]`,中文显示名/路径无乱码,`status`/`start_type` 为英文枚举;对 LNX/MAC 主机返回 `-32005`。
|
||||||
|
- MCP `get_client_log` 返回 `log` 文本快照(UTF-8),中文日志无乱码;空日志返回空 `log`。
|
||||||
|
- 错误路径:缺 `id`/非数字 `id` → `-32602`;未知/离线主机 → `-32002`;同 host 并发 → `-32003`;发送失败 → `-32004`;超时 → `-32001`。
|
||||||
|
|
||||||
|
**验证记录**(2026-08-19,真实在线主机实测):
|
||||||
|
- `list_services`(Windows 11 EXE 主机「Essential / VCU」):返回 255 个服务,字段序(显示名→服务名→路径→状态→启动类型)正确,`status`/`start_type` 英文枚举、路径完整。
|
||||||
|
- `get_client_log`(同上主机):返回 510 行 / 40309 字节全量快照,`[pid][时间戳][文件:行号]` 格式正确,中文日志(「启动运行」「任务已存在」「打开注册表失败,错误码:2」等)GBK→UTF-8 转码全部正确。
|
||||||
|
- 错误路径实测:LNX 主机 `list_services` → `-32005`;缺/非法 `id` → `-32602`;离线 → `-32002`。
|
||||||
|
- **客户端版本门槛**:`get_client_log` 对 Linux 客户端 20s 超时返回 `-32001`——当前 Linux 客户端运行版本未实现 `COMMAND_QUERY_LOG`→`CClientLogManager` 日志回传链路(服务端下发命令无响应),属版本/实现门槛,非 MCP 缺陷(与 §6.2 `get_activity_history` 对老客户端超时同类)。`list_services` 因服务为 Windows-only(`EnumServicesStatus`),已对 LNX/MAC 提前 `-32005`,不受此门槛影响。
|
||||||
|
|
||||||
|
**回滚**:revert `McpServer.h` + `McpServer.cpp` + `MessageHandle` 两处拦截。
|
||||||
|
|
||||||
|
### 6.5 P3a:`exec_command`(模式 B,写操作,评审后实施)
|
||||||
|
|
||||||
|
**改动**:复刻 Web 终端链路(§4.3),把 `m_TermPending` 换成 MCP 自己的挂起标记:
|
||||||
|
- 下 `COMMAND_SHELL` → `TOKEN_SHELL_START`/`TOKEN_SHELL_DATA` 接管 → 发送命令文本 → 收集 stdout → 关子链接 → 返回。
|
||||||
|
|
||||||
|
**完成判据(本工具最难的半截,必须定死)**:交互 shell 没有「命令结束」信号,采用**哨兵方案**:
|
||||||
|
|
||||||
|
```
|
||||||
|
实际下发: cmd ; echo __MCP_DONE_<nonce>__ ; echo __MCP_EXIT_$?__
|
||||||
|
收集端: 持续累积 TOKEN_SHELL_DATA,直到同时匹配 __MCP_DONE_<nonce>__ 与 __MCP_EXIT_<code>__
|
||||||
|
取出退出码,剥离哨兵行,返回 stdout
|
||||||
|
```
|
||||||
|
|
||||||
|
`<nonce>` 随机,避免命令自身输出误命中哨兵;整体受 `McpToolTimeoutMs` 兜底(超时强制关链 + 返回错误)。
|
||||||
|
|
||||||
|
**前置安全评审**(见 §8)通过后才实施。
|
||||||
|
|
||||||
|
**验收**:`exec_command(id, "tasklist")` 返回进程列表文本 + 退出码;白名单外命令被拒绝并审计;超时/离线返回明确错误;MFC 终端弹框不受影响。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 「新增工具」标准配方(可复用流程)
|
||||||
|
|
||||||
|
后续任何人新增一个工具,按以下 6 步走:
|
||||||
|
|
||||||
|
1. **定协议与模式**:确认复用哪个 `COMMAND_*`→`TOKEN_*` 链路,判定是模式 A(主连接 RPC)、A0(单向命令)还是 B(子链接流式),写进 §5.1 表格。
|
||||||
|
2. **定 schema**:在 `BuildToolsListResult` 注册 `name`/`description`/`inputSchema`/`outputSchema`(description 用 `u8"..."`,见 §10)。
|
||||||
|
3. **加 dispatch**:在 `BuildToolsCall` 里加一个 `toolName == "..."` 分支。
|
||||||
|
4. **实现数据通路**:
|
||||||
|
- 纯内存/现成字段 → 直接实现,不进 `MessageHandle`。
|
||||||
|
- 模式 A0(单向命令)→ 直接 `Send2Client` 后返回,无需挂起/超时。
|
||||||
|
- 模式 A → 加 `m_Pending` 挂起 + `MessageHandle` 对应 `TOKEN_*` 加 `if (IsPending) TakeMainResponse`(IO 线程同步拷贝,**不 CancelIO**)。
|
||||||
|
- 模式 A′ → 同模式 A,但拦截后额外 `ContextObject->CancelIO()` 关一次性子链接。
|
||||||
|
- 模式 B → 加 `m_Pending` 挂起 + `MessageHandle` 对应 `TOKEN_*` 加 `if (IsPending) TakeSubContext` + `OnShellData` 泵数据。
|
||||||
|
5. **超时与清理**:带超时、迟到回收、用完即关(模式 B);每 host 单飞行(§4.5)。
|
||||||
|
6. **验收与回滚**:写 §11 的 DoD 清单,确认回归点(对应 MFC 弹框路径不变)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 安全与权限模型演进
|
||||||
|
|
||||||
|
| 阶段 | 鉴权 | 授权 |
|
||||||
|
|------|------|------|
|
||||||
|
| P1/P2 | 单一静态 Bearer token | 全部只读,token 即授权 |
|
||||||
|
| P3 | 引入 **token 分级 + 分组授权** | 只读 token / 完整 token 两档;写操作要求完整 token 且限定可访问分组 |
|
||||||
|
| P4 | (如需)会话级授权 | 会话内临时授权 |
|
||||||
|
|
||||||
|
**P3 进入前必须评审的三条边界:**
|
||||||
|
|
||||||
|
1. **命令白名单**:`exec_command` 默认只放行只读命令(`systeminfo`/`tasklist`/`ipconfig`/`netstat` 等);危险命令(`del`/`format`/下载执行/`reg add`)需显式放行或直接拒绝。
|
||||||
|
2. **审计日志**:每次写操作记录 `host_id + tool + 参数 + token 来源(env/CFG)+ 时间`,写 `Mprintf` + 日志文件。
|
||||||
|
3. **分组授权(新增)**:写工具必须限定「token 能操作哪些主机/分组」,**复用 Web 远程桌面已有的 `WebUser.allowed_groups` + admin/viewer 角色模型**(`WebService.cpp:1852-1877`),而非仅区分读/写。否则一个完整 token 就能对**所有**主机执行命令,越过了 Web 侧已有的分组隔离。
|
||||||
|
|
||||||
|
> 设计底线:MCP 的写能力与 Web 远程桌面同等级敏感,任何写工具都要按「可控、可审、可关、可分组隔离」交付,不允许「开了 MCP 就能无脑执行任意命令」。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 配置与开关扩展
|
||||||
|
|
||||||
|
延续 Phase 1 的配置风格(`settings` 段 + env 覆盖 + `UIBranding` 开关):
|
||||||
|
|
||||||
|
> **配置存储位置(重要)**:`THIS_CFG` 在 **Release 版恒走注册表** `HKCU\Software\YAMA\settings`(`2015Remote.cpp:238` 的 `#else` 分支 `new iniFile`);`settings.ini` 仅在 **Debug 版**(`#ifdef _DEBUG` 且 `GetPwdHash()==masterHash`)才读,否则同样走注册表。故 Release 版改 MCP 配置应走「扩展 → MCP设置」对话框(写注册表)或直接改注册表,手改 `settings.ini` 对 Release 无效。
|
||||||
|
|
||||||
|
| 配置键 | 默认 | 说明 |
|
||||||
|
|--------|------|------|
|
||||||
|
| `McpEnabled` | 0 | 总开关(已有) |
|
||||||
|
| `McpPort` | 6544 | 端口(已有) |
|
||||||
|
| `McpBind` | 127.0.0.1 | 绑定(已有) |
|
||||||
|
| `McpToken` | 空 | 静态 token(已有) |
|
||||||
|
| `McpReadonly` | 1 | **P3 新增**:1=仅只读工具,0=开放写工具(配合完整 token) |
|
||||||
|
| `McpCmdWhitelist` | 内置只读命令集 | **P3 新增**:`exec_command` 白名单(分号分隔) |
|
||||||
|
| `McpToolTimeoutMs` | 20000 | **P2b 新增**:数据等待超时(当前为 `McpServer.cpp` 内编译期常量 `kMcpToolTimeoutMs`,未接配置) |
|
||||||
|
|
||||||
|
新增写能力时,`UIBranding.h`/`FeatureFlags.h` 加对应隐藏/特性位,保持「品牌定制可裁剪」的既有约定。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 编码 / 兼容性规范
|
||||||
|
|
||||||
|
- **UTF-8 输出**:所有进入 JSON 的字符串用 `u8"..."` 前缀(项目 `/execution-charset:.936` 会把普通窄字面量编成 GBK)。已在 `list_online_hosts` 的 `description` 中验证,服务端输出正确 UTF-8。
|
||||||
|
- **JSON 转义**:jsoncpp 会把非 ASCII 输出为 `\uXXXX`,这是合法 JSON,客户端解析即还原,无需处理。
|
||||||
|
- **客户端能力位**:依赖客户端能力的工具(如 `get_screenshot` 的 `CLIENT_CAP_SCREEN_PREVIEW`、UTF-8 字段的 `CLIENT_CAP_UTF8`)必须先查能力位,不支持则返回明确错误而非空结果。
|
||||||
|
- **id 一致性**:工具入参 `id` 必须与 `list_online_hosts` 返回的 `id` 同源(`context::GetClientID()` 的 uint64),可直接回填 `FindHost(uint64_t)`;仅接受**在线**主机的 id,离线返回明确错误。
|
||||||
|
- **版本兼容**:子链接命令/结构体(`common/commands.h`)**禁止改动既有字段**;MCP 只消费既有协议,不新增命令(原则 2)。
|
||||||
|
- **MBCS 构建**:新增 UI 文案走 `_TR()` + lang ini(GBK),与 Phase 1 一致。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 验收标准(Definition of Done)
|
||||||
|
|
||||||
|
每个工具交付时,以下全部满足才算完成:
|
||||||
|
|
||||||
|
- [ ] `tools/list` 返回该工具及完整 schema。
|
||||||
|
- [ ] 正常输入返回正确结构化结果。
|
||||||
|
- [ ] 非法输入(错 id / 缺参数 / 设备离线 / 不支持能力)返回**明确的 JSON-RPC 错误码**,不崩溃、不挂死。
|
||||||
|
- [ ] 数据获取有超时、迟到回收;模式 B 用完即关(无会话泄漏)。
|
||||||
|
- [ ] 每 host 单飞行:同 host 并发请求被拒绝而非串扰。
|
||||||
|
- [ ] **回归点**:对应 MFC 弹框/Web 路径行为不变(非 MCP 触发时不受影响)。
|
||||||
|
- [ ] 编码正确(UTF-8,中文无乱码)。
|
||||||
|
- [ ] 写操作工具附带白名单/审计/分组授权(仅 P3)。
|
||||||
|
- [ ] 无新增第三方库。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. 风险与回滚
|
||||||
|
|
||||||
|
| 风险 | 缓解 |
|
||||||
|
|------|------|
|
||||||
|
| 无头接管串扰 MFC/Web 路径 | 挂起标记按 `device_id` 唯一;`if (IsPending)` 仅在 MCP 触发时成立,其余路径原样 |
|
||||||
|
| 共享 `InDeCompressedBuffer` 被覆盖 | 模式 A 在 IO 线程 `MessageHandle` 内同步拷贝,不等异步 |
|
||||||
|
| 无请求 id 的工具响应串扰 | 每 host 单飞行(§4.4);MCP 与 MFC 各用独立子链接,互不占位(§4.2′) |
|
||||||
|
| 阻塞 httplib worker 饿死后续请求 | 低频串行可接受;文档显式声明,未来并发改「任务+轮询」(§4.5) |
|
||||||
|
| 超时/挂死 | 所有工具强制 `wait_for(timeout)` + 迟到回收 |
|
||||||
|
| 写操作越权 | P3 前三道评审边界(白名单/审计/分组授权)+ `McpReadonly` 默认 1 |
|
||||||
|
| 一次性大改影响现有功能 | 分 P2a→P2b→P2c→P3a 四期,每期独立合入/验收/回滚 |
|
||||||
|
| 输出过大拖垮 JSON | `list_files`/进程列表等大结果设上限/分页 |
|
||||||
|
|
||||||
|
**回滚策略**:每期改动集中在一两个文件(`McpServer.*` + 可选 `MessageHandle` 若干 `if` 分支),出错 revert 当期 commit 即可,不影响已交付的 P1。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附:建议执行顺序
|
||||||
|
|
||||||
|
1. **P2a**(`search_hosts` + `get_host_detail`)✅ ——纯内存、零风险,已实现,验证「多工具 dispatch」模式。
|
||||||
|
2. **P2b**(`list_processes` + `list_windows` + `get_activity_history`)✅ ——首次引入**模式 A′ 一次性子链接**(进程/窗口)与**模式 A 主连接 RPC**(历史活动)无头接管,把 §4.2/§4.2′ 的机制跑通并沉淀成 §7 配方。
|
||||||
|
3. **P2c**(`get_screenshot` 走屏幕预览链路 + `list_files`)✅ ——补上「看」的能力,仍是只读(`get_screenshot` 模式 A、`list_files` 模式 A′)。
|
||||||
|
4. **P2d**(`list_services` + `get_client_log`)✅ ——补上「看服务 / 看日志」的感知能力,仍是只读(均为模式 A′,复用 P2b/P2c 的一次性子链接机制)。
|
||||||
|
5. **P3a**(`exec_command`)——首次引入**模式 B 子链接流式**,在 P2 的挂起机制成熟后,走完安全评审再落地写操作。
|
||||||
|
|
||||||
|
每完成一期即提交、验收,再进入下一期,保持「有序、平稳、循序渐进」。
|
||||||
Binary file not shown.
@@ -15,6 +15,7 @@
|
|||||||
#include "TerminalDlg.h"
|
#include "TerminalDlg.h"
|
||||||
#include "SystemDlg.h"
|
#include "SystemDlg.h"
|
||||||
#include "CClientLog.h"
|
#include "CClientLog.h"
|
||||||
|
#include "CActivityDialog.h"
|
||||||
#include "BuildDlg.h"
|
#include "BuildDlg.h"
|
||||||
#include "AudioDlg.h"
|
#include "AudioDlg.h"
|
||||||
#include "RegisterDlg.h"
|
#include "RegisterDlg.h"
|
||||||
@@ -42,6 +43,8 @@
|
|||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include "common/skCrypter.h"
|
#include "common/skCrypter.h"
|
||||||
#include "common/commands.h"
|
#include "common/commands.h"
|
||||||
|
#include "McpServer.h"
|
||||||
|
#include "McpSettingsDlg.h"
|
||||||
#include "common/md5.h"
|
#include "common/md5.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <set>
|
#include <set>
|
||||||
@@ -148,10 +151,14 @@ COLUMNSTRUCT g_Column_Data_Online[g_Column_Count_Online] = {
|
|||||||
{"摄像头", 60 },
|
{"摄像头", 60 },
|
||||||
{"RTT", 60 },
|
{"RTT", 60 },
|
||||||
{"版本", 90 },
|
{"版本", 90 },
|
||||||
{"权限", 60 },
|
|
||||||
{"安装时间", 130 },
|
{"安装时间", 130 },
|
||||||
{"活动窗口", 130 },
|
{"活动窗口", 130 },
|
||||||
{"类型", 50 },
|
{"类型", 50 },
|
||||||
|
{"", 0 },
|
||||||
|
{"", 0 },
|
||||||
|
{"", 0 },
|
||||||
|
{"", 0 },
|
||||||
|
{"权限", 60 },
|
||||||
};
|
};
|
||||||
|
|
||||||
// 缩略图列号常量 + 列号→数据槽映射。负返回值代表"该列无数据槽"。
|
// 缩略图列号常量 + 列号→数据槽映射。负返回值代表"该列无数据槽"。
|
||||||
@@ -196,10 +203,10 @@ bool SupportsFileTransferV2(context* ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取客户端协议字符串编码:优先看自身能力位,若是子连接(CAPABILITIES 为空)
|
// 获取客户端协议字符串编码:优先看自身能力位,若是子连接(CAPABILITIES 为空)
|
||||||
// 则通过 peer IP 查主连接。Linux/macOS 客户端文件系统路径与 locale 现代发行版
|
// 则先按 clientID、再按 peer IP 查主连接。Linux/macOS 客户端文件系统路径与 locale
|
||||||
// 默认就是 UTF-8——即便客户端二进制是早于 CLIENT_CAP_UTF8 引入(commit 0aa7588)
|
// 现代发行版默认就是 UTF-8——即便客户端二进制是早于 CLIENT_CAP_UTF8 引入
|
||||||
// 之前编译的,没声明 cap 位,事实上仍发 UTF-8 字节,按 client type 兜底走 UTF-8。
|
// (commit 0aa7588)之前编译的,没声明 cap 位,事实上仍发 UTF-8 字节,按 client
|
||||||
// 找不到则默认 CP936。
|
// type 兜底走 UTF-8。找不到则默认 CP936。
|
||||||
UINT GetClientEncoding(context* ctx) {
|
UINT GetClientEncoding(context* ctx) {
|
||||||
if (!ctx) return 936;
|
if (!ctx) return 936;
|
||||||
// 主连接情形:CAPABILITIES 已由 LOGIN_INFOR 处理流程填好
|
// 主连接情形:CAPABILITIES 已由 LOGIN_INFOR 处理流程填好
|
||||||
@@ -207,9 +214,17 @@ UINT GetClientEncoding(context* ctx) {
|
|||||||
// 客户端类型兜底:LNX / MAC 默认 UTF-8(兼容老二进制无 UTF-8 cap 位的情形)
|
// 客户端类型兜底:LNX / MAC 默认 UTF-8(兼容老二进制无 UTF-8 cap 位的情形)
|
||||||
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
|
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
|
||||||
if (clientType == "LNX" || clientType == "MAC") return CP_UTF8;
|
if (clientType == "LNX" || clientType == "MAC") return CP_UTF8;
|
||||||
// 子连接情形:CAPABILITIES 为空 -> 通过 IP 找主连接
|
// 子连接情形:CAPABILITIES 为空 -> 找主连接。
|
||||||
|
// 优先按 clientID 精确查:子连接经 TOKEN_CONN_AUTH 校验后 SetID(pkt->clientID)
|
||||||
|
// 已把 clientID 钉成主连接 ID,FRP/NAT 反代下 socket peer 是 127.0.0.1/内网地址
|
||||||
|
// 也能命中,不再依赖 peer IP 反查。
|
||||||
if (g_2015RemoteDlg) {
|
if (g_2015RemoteDlg) {
|
||||||
context* mainCtx = g_2015RemoteDlg->FindHostByIP(ctx->GetPeerName());
|
context* mainCtx = g_2015RemoteDlg->FindHostByClientID(ctx->GetClientID());
|
||||||
|
if (!mainCtx) {
|
||||||
|
// 老客户端子连接不发 TOKEN_CONN_AUTH(clientID 未钉,为 0),
|
||||||
|
// 回退到 peer IP 反查(直连场景仍可命中)。
|
||||||
|
mainCtx = g_2015RemoteDlg->FindHostByIP(ctx->GetPeerName());
|
||||||
|
}
|
||||||
if (mainCtx) {
|
if (mainCtx) {
|
||||||
if (mainCtx->SupportsUtf8()) return CP_UTF8;
|
if (mainCtx->SupportsUtf8()) return CP_UTF8;
|
||||||
CString mainType = mainCtx->GetAdditionalData(RES_CLIENT_TYPE);
|
CString mainType = mainCtx->GetAdditionalData(RES_CLIENT_TYPE);
|
||||||
@@ -648,6 +663,9 @@ CMy2015RemoteDlg::CMy2015RemoteDlg(CWnd* pParent): CDialogLangEx(CMy2015RemoteDl
|
|||||||
m_bmOnline[60].LoadBitmap(IDB_BITMAP_DEBUG);
|
m_bmOnline[60].LoadBitmap(IDB_BITMAP_DEBUG);
|
||||||
m_bmOnline[61].LoadBitmap(IDB_BITMAP_CLIENTLOG);
|
m_bmOnline[61].LoadBitmap(IDB_BITMAP_CLIENTLOG);
|
||||||
m_bmOnline[62].LoadBitmap(IDB_BITMAP_FORBIDDEN);
|
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++) {
|
for (int i = 0; i < PAYLOAD_MAXTYPE; i++) {
|
||||||
m_ServerDLL[i] = nullptr;
|
m_ServerDLL[i] = nullptr;
|
||||||
m_ServerBin[i] = nullptr;
|
m_ServerBin[i] = nullptr;
|
||||||
@@ -938,6 +956,9 @@ 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_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_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)
|
||||||
@@ -965,6 +986,7 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
|
|||||||
ON_COMMAND(ID_SHELLCODE_TEST_AES_BIN, &CMy2015RemoteDlg::OnShellcodeTestAesBin)
|
ON_COMMAND(ID_SHELLCODE_TEST_AES_BIN, &CMy2015RemoteDlg::OnShellcodeTestAesBin)
|
||||||
ON_COMMAND(ID_TOOL_RELOAD_PLUGINS, &CMy2015RemoteDlg::OnToolReloadPlugins)
|
ON_COMMAND(ID_TOOL_RELOAD_PLUGINS, &CMy2015RemoteDlg::OnToolReloadPlugins)
|
||||||
ON_COMMAND(ID_TOOL_PLUGIN_SETTINGS, &CMy2015RemoteDlg::OnToolPluginSettings)
|
ON_COMMAND(ID_TOOL_PLUGIN_SETTINGS, &CMy2015RemoteDlg::OnToolPluginSettings)
|
||||||
|
ON_COMMAND(ID_MCP_SETTINGS, &CMy2015RemoteDlg::OnMcpSettings)
|
||||||
ON_COMMAND(ID_TRIGGER_SETTINGS, &CMy2015RemoteDlg::OnTriggerSettings)
|
ON_COMMAND(ID_TRIGGER_SETTINGS, &CMy2015RemoteDlg::OnTriggerSettings)
|
||||||
ON_COMMAND(ID_SHELLCODE_AES_C_ARRAY, &CMy2015RemoteDlg::OnShellcodeAesCArray)
|
ON_COMMAND(ID_SHELLCODE_AES_C_ARRAY, &CMy2015RemoteDlg::OnShellcodeAesCArray)
|
||||||
ON_COMMAND(ID_PARAM_KBLOGGER, &CMy2015RemoteDlg::OnParamKblogger)
|
ON_COMMAND(ID_PARAM_KBLOGGER, &CMy2015RemoteDlg::OnParamKblogger)
|
||||||
@@ -1010,6 +1032,7 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
|
|||||||
ON_COMMAND(ID_ONLINE_VIEW_WND, &CMy2015RemoteDlg::OnOnlineWindowManager)
|
ON_COMMAND(ID_ONLINE_VIEW_WND, &CMy2015RemoteDlg::OnOnlineWindowManager)
|
||||||
ON_COMMAND(ID_ENABLE_DEV_DEBUG, &CMy2015RemoteDlg::OnEnableDevDebug)
|
ON_COMMAND(ID_ENABLE_DEV_DEBUG, &CMy2015RemoteDlg::OnEnableDevDebug)
|
||||||
ON_COMMAND(ID_ONLINE_CLIENT_LOG, &CMy2015RemoteDlg::OnOnlineClientLog)
|
ON_COMMAND(ID_ONLINE_CLIENT_LOG, &CMy2015RemoteDlg::OnOnlineClientLog)
|
||||||
|
ON_COMMAND(ID_ONLINE_HISTORY_ACTIVITY, &CMy2015RemoteDlg::OnOnlineHistoryActivity)
|
||||||
ON_COMMAND(ID_ONLINE_FORBIDDEN, &CMy2015RemoteDlg::OnOnlineForbidden)
|
ON_COMMAND(ID_ONLINE_FORBIDDEN, &CMy2015RemoteDlg::OnOnlineForbidden)
|
||||||
END_MESSAGE_MAP()
|
END_MESSAGE_MAP()
|
||||||
|
|
||||||
@@ -1205,13 +1228,63 @@ VOID CMy2015RemoteDlg::CreateSolidMenu()
|
|||||||
pExtMenu->DeleteMenu(ID_BACKUP_DATA, MF_BYCOMMAND);
|
pExtMenu->DeleteMenu(ID_BACKUP_DATA, MF_BYCOMMAND);
|
||||||
if (SHOULD_HIDE_MENU(HIDE_MENU_IMPORT_DATA, MF_IMPORT_DATA))
|
if (SHOULD_HIDE_MENU(HIDE_MENU_IMPORT_DATA, MF_IMPORT_DATA))
|
||||||
pExtMenu->DeleteMenu(ID_IMPORT_DATA, MF_BYCOMMAND);
|
pExtMenu->DeleteMenu(ID_IMPORT_DATA, MF_BYCOMMAND);
|
||||||
if (SHOULD_HIDE_MENU(HIDE_MENU_RELOAD_PLUGINS, MF_RELOAD_PLUGINS))
|
|
||||||
pExtMenu->DeleteMenu(ID_TOOL_RELOAD_PLUGINS, MF_BYCOMMAND);
|
|
||||||
if (SHOULD_HIDE_MENU(HIDE_MENU_PLUGIN_REQUEST, MF_PLUGIN_REQUEST))
|
|
||||||
pExtMenu->DeleteMenu(ID_PLUGIN_REQUEST, MF_BYCOMMAND);
|
|
||||||
if (SHOULD_HIDE_MENU(HIDE_MENU_FRPS_FOR_SUB, MF_FRPS_FOR_SUB))
|
if (SHOULD_HIDE_MENU(HIDE_MENU_FRPS_FOR_SUB, MF_FRPS_FOR_SUB))
|
||||||
pExtMenu->DeleteMenu(ID_FRPS_FOR_SUB, MF_BYCOMMAND);
|
pExtMenu->DeleteMenu(ID_FRPS_FOR_SUB, MF_BYCOMMAND);
|
||||||
|
|
||||||
|
// 动态插入「MCP设置」菜单项(2015Remote.rc 为 UTF-16 不便编辑,改运行时插入,
|
||||||
|
// 位于「地理信息」之后、「插件设置」之前,见 docs/Mcp_Design.md §3.8.1)
|
||||||
|
if (!SHOULD_HIDE_MENU(HIDE_MENU_MCP_SETTINGS, MF_MCP_SETTINGS)) {
|
||||||
|
UINT mcpFlags = MF_BYCOMMAND;
|
||||||
|
if (THIS_CFG.GetInt("settings", "McpEnabled", 0)) {
|
||||||
|
mcpFlags |= MF_CHECKED; // 已启用 MCP 则菜单打勾
|
||||||
|
}
|
||||||
|
pExtMenu->InsertMenu(ID_TOOL_PLUGIN_SETTINGS, mcpFlags,
|
||||||
|
ID_MCP_SETTINGS, _TR("MCP设置(&M)..."));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将 3 个插件相关菜单项收拢到「插件」二级菜单(位于「MCP设置」之下、原「插件设置」之前,
|
||||||
|
// 顺序不变:插件设置 / 刷新插件 / 获取插件)。
|
||||||
|
// 注意:先删除顶层项、再按位置插入子菜单,避免同一命令 ID 同时存在于子菜单与顶层时,
|
||||||
|
// MF_BYCOMMAND 的 DeleteMenu/InsertMenu 因重复 ID 而误删/误定位子菜单项。
|
||||||
|
{
|
||||||
|
// 记录「插件设置」的索引,作为「插件」子菜单的插入点
|
||||||
|
int pluginPos = -1;
|
||||||
|
for (UINT i = 0; i < pExtMenu->GetMenuItemCount(); i++) {
|
||||||
|
if (pExtMenu->GetMenuItemID(i) == ID_TOOL_PLUGIN_SETTINGS) {
|
||||||
|
pluginPos = (int)i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pluginPos >= 0) {
|
||||||
|
// 先删除 3 个顶层项
|
||||||
|
pExtMenu->DeleteMenu(ID_TOOL_PLUGIN_SETTINGS, MF_BYCOMMAND);
|
||||||
|
pExtMenu->DeleteMenu(ID_TOOL_RELOAD_PLUGINS, MF_BYCOMMAND);
|
||||||
|
pExtMenu->DeleteMenu(ID_PLUGIN_REQUEST, MF_BYCOMMAND);
|
||||||
|
|
||||||
|
// 再创建「插件」子菜单
|
||||||
|
CMenu pluginMenu;
|
||||||
|
pluginMenu.CreatePopupMenu();
|
||||||
|
pluginMenu.AppendMenu(MF_STRING, ID_TOOL_PLUGIN_SETTINGS, _TR("插件设置(&S)"));
|
||||||
|
if (!SHOULD_HIDE_MENU(HIDE_MENU_RELOAD_PLUGINS, MF_RELOAD_PLUGINS))
|
||||||
|
pluginMenu.AppendMenu(MF_STRING, ID_TOOL_RELOAD_PLUGINS, _TR("刷新插件(&P)"));
|
||||||
|
if (!SHOULD_HIDE_MENU(HIDE_MENU_PLUGIN_REQUEST, MF_PLUGIN_REQUEST))
|
||||||
|
pluginMenu.AppendMenu(MF_STRING, ID_PLUGIN_REQUEST, _TR("获取插件(&P)"));
|
||||||
|
|
||||||
|
// 继承原 3 项的菜单图标(见上方 SetMenuItemBitmaps 区)
|
||||||
|
pluginMenu.SetMenuItemBitmaps(ID_TOOL_PLUGIN_SETTINGS, MF_BYCOMMAND, &m_bmOnline[53], &m_bmOnline[53]);
|
||||||
|
if (!SHOULD_HIDE_MENU(HIDE_MENU_RELOAD_PLUGINS, MF_RELOAD_PLUGINS))
|
||||||
|
pluginMenu.SetMenuItemBitmaps(ID_TOOL_RELOAD_PLUGINS, MF_BYCOMMAND, &m_bmOnline[43], &m_bmOnline[43]);
|
||||||
|
if (!SHOULD_HIDE_MENU(HIDE_MENU_PLUGIN_REQUEST, MF_PLUGIN_REQUEST))
|
||||||
|
pluginMenu.SetMenuItemBitmaps(ID_PLUGIN_REQUEST, MF_BYCOMMAND, &m_bmOnline[44], &m_bmOnline[44]);
|
||||||
|
|
||||||
|
// 插入到原「插件设置」位置(MF_BYPOSITION)
|
||||||
|
pExtMenu->InsertMenu((UINT)pluginPos, MF_BYPOSITION | MF_POPUP,
|
||||||
|
(UINT_PTR)pluginMenu.GetSafeHmenu(), _TR("插件(&P)"));
|
||||||
|
pluginMenu.Detach(); // HMENU 所有权转交 pExtMenu,避免局部析构销毁子菜单
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- 扩展菜单 - 语言设置子菜单 ---
|
// --- 扩展菜单 - 语言设置子菜单 ---
|
||||||
CMenu* pLangMenu = FindSubMenuByCommand(pExtMenu, ID_CHANGE_LANG);
|
CMenu* pLangMenu = FindSubMenuByCommand(pExtMenu, ID_CHANGE_LANG);
|
||||||
if (pLangMenu) {
|
if (pLangMenu) {
|
||||||
@@ -1412,6 +1485,13 @@ VOID CMy2015RemoteDlg::InitControl()
|
|||||||
g_Column_Online_Width+=g_Column_Data_Online[i].nWidth;
|
g_Column_Online_Width+=g_Column_Data_Online[i].nWidth;
|
||||||
}
|
}
|
||||||
m_CList_Online.InitColumns();
|
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 工程里
|
// 让虚拟列表用 Unicode 通知(LVN_GETDISPINFOW),从而绕开 MBCS 工程里
|
||||||
// 列表 → ANSI → CP_ACP 的回转,这样在德语/日语等非中文 ACP 服务端上也能显示中文。
|
// 列表 → ANSI → CP_ACP 的回转,这样在德语/日语等非中文 ACP 服务端上也能显示中文。
|
||||||
m_CList_Online.SendMessage(LVM_SETUNICODEFORMAT, TRUE, 0);
|
m_CList_Online.SendMessage(LVM_SETUNICODEFORMAT, TRUE, 0);
|
||||||
@@ -1487,8 +1567,8 @@ VOID CMy2015RemoteDlg::AddList(CString strIP, CString strAddr, CString strPCName
|
|||||||
CString privilegeStr = privUser == "SYSTEM" ? "SYSTEM" : privAdmin == "1" ? "Admin" : "User";
|
CString privilegeStr = privUser == "SYSTEM" ? "SYSTEM" : privAdmin == "1" ? "Admin" : "User";
|
||||||
|
|
||||||
CString data[ONLINELIST_MAX] = { strIP, strAddr, "", strPCName, strOS, strCPU, strVideo, strPing,
|
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,
|
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,
|
v[RES_CLIENT_PUBIP].empty() ? strIP : v[RES_CLIENT_PUBIP].c_str(), startTime, capStr, privilegeStr,
|
||||||
};
|
};
|
||||||
// 优先采用客户端自报的 ID(新客户端用 V2 算法 = MachineGuid + 归一化路径,
|
// 优先采用客户端自报的 ID(新客户端用 V2 算法 = MachineGuid + 归一化路径,
|
||||||
// 比服务端按老算法 IP+PC+OS+CPU+PATH 重算更稳定)。
|
// 比服务端按老算法 IP+PC+OS+CPU+PATH 重算更稳定)。
|
||||||
@@ -2061,6 +2141,34 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start MCP service(默认禁用;经「扩展 → MCP设置」开启后监听,见 docs/Mcp_Design.md §3.7)
|
||||||
|
if (THIS_CFG.GetInt("settings", "McpEnabled", 0)) {
|
||||||
|
auto mcpPort = THIS_CFG.GetInt("settings", "McpPort", 6544);
|
||||||
|
auto mcpBind = THIS_CFG.GetStr("settings", "McpBind", "127.0.0.1");
|
||||||
|
|
||||||
|
// token 优先顺序:env BRAND_MCP_ENV_VAR → THIS_CFG McpToken → 皆空则随机(仅本次进程有效)
|
||||||
|
const char* envTok = getenv(BRAND_MCP_ENV_VAR);
|
||||||
|
std::string mcpToken;
|
||||||
|
if (envTok && *envTok) {
|
||||||
|
mcpToken = envTok;
|
||||||
|
} else {
|
||||||
|
mcpToken = THIS_CFG.GetStr("settings", "McpToken", "");
|
||||||
|
}
|
||||||
|
if (mcpToken.empty()) {
|
||||||
|
mcpToken = GenerateRandomToken();
|
||||||
|
Mprintf("[McpServer] %s / McpToken 均未设置,本次进程随机 token:%s\n",
|
||||||
|
BRAND_MCP_ENV_VAR, mcpToken.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
McpServer().SetParentDlg(this);
|
||||||
|
McpServer().SetToken(mcpToken);
|
||||||
|
if (!McpServer().Start(mcpBind, mcpPort)) {
|
||||||
|
Mprintf("McpServer start failed on %s:%d\n", mcpBind.c_str(), mcpPort);
|
||||||
|
} else {
|
||||||
|
Mprintf("McpServer started on %s:%d\n", mcpBind.c_str(), mcpPort);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
UPDATE_SPLASH(18, "正在初始化文件上传模块...");
|
UPDATE_SPLASH(18, "正在初始化文件上传模块...");
|
||||||
int ret = InitFileUpload({}, {}, GetHMAC(), 64, 50, Logf, GetUpperHash());
|
int ret = InitFileUpload({}, {}, GetHMAC(), 64, 50, Logf, GetUpperHash());
|
||||||
g_hKeyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, AfxGetInstanceHandle(), 0);
|
g_hKeyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, AfxGetInstanceHandle(), 0);
|
||||||
@@ -3166,6 +3274,12 @@ void CMy2015RemoteDlg::ApplyFrpSettings()
|
|||||||
auto arr = StringToVector(ports, ';');
|
auto arr = StringToVector(ports, ';');
|
||||||
int fileServerPort = THIS_CFG.GetInt("settings", "WebSvrPort", 8080);
|
int fileServerPort = THIS_CFG.GetInt("settings", "WebSvrPort", 8080);
|
||||||
|
|
||||||
|
// MCP 端口:仅当「启用 MCP 且绑定 0.0.0.0」时纳入 FRP 暴露(与监听/Web 取并集,不覆盖)。
|
||||||
|
int mcpEnabled = THIS_CFG.GetInt("settings", "McpEnabled", 0);
|
||||||
|
std::string mcpBind = THIS_CFG.GetStr("settings", "McpBind", "127.0.0.1");
|
||||||
|
int mcpPort = THIS_CFG.GetInt("settings", "McpPort", 6544);
|
||||||
|
bool exposeMcp = (mcpEnabled != 0) && (mcpBind == "0.0.0.0") && (mcpPort > 0);
|
||||||
|
|
||||||
// 为每个服务端生成独立配置文件 (index=0 用 frpc.ini 保持兼容)
|
// 为每个服务端生成独立配置文件 (index=0 用 frpc.ini 保持兼容)
|
||||||
for (size_t idx = 0; idx < servers.size(); ++idx) {
|
for (size_t idx = 0; idx < servers.size(); ++idx) {
|
||||||
std::string path = (idx == 0) ? basePath : baseNoExt + "." + std::to_string(idx) + ".ini";
|
std::string path = (idx == 0) ? basePath : baseNoExt + "." + std::to_string(idx) + ".ini";
|
||||||
@@ -3195,6 +3309,14 @@ void CMy2015RemoteDlg::ApplyFrpSettings()
|
|||||||
cfg.SetInt(name, "remote_port", fileServerPort);
|
cfg.SetInt(name, "remote_port", fileServerPort);
|
||||||
cfg.SetStr(name, "proxy_protocol_version", "v2"); // 传递真实客户端 IP
|
cfg.SetStr(name, "proxy_protocol_version", "v2"); // 传递真实客户端 IP
|
||||||
}
|
}
|
||||||
|
if (exposeMcp) {
|
||||||
|
std::string name = BRAND_NET_PREFIX "-MCP-" + std::to_string(mcpPort);
|
||||||
|
cfg.SetStr(name, "type", "tcp");
|
||||||
|
cfg.SetInt(name, "local_port", mcpPort);
|
||||||
|
cfg.SetInt(name, "remote_port", mcpPort);
|
||||||
|
// 不设置 proxy_protocol_version:MCP 由 httplib 提供、不解析 Proxy Protocol,
|
||||||
|
// 且 MCP 以 Bearer token 鉴权、无需透传真实 IP(监听/Web 端口才需要 PP2)。
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3473,6 +3595,7 @@ void CMy2015RemoteDlg::OnTimer(UINT_PTR nIDEvent)
|
|||||||
|
|
||||||
// 虚拟列表:重建过滤索引并更新项目计数
|
// 虚拟列表:重建过滤索引并更新项目计数
|
||||||
if (hasListChange) {
|
if (hasListChange) {
|
||||||
|
ApplySort(); // 有排序偏好时重排,让新上线主机落到排序位置
|
||||||
RebuildFilteredIndices();
|
RebuildFilteredIndices();
|
||||||
m_CList_Online.SetItemCountExV((int)m_FilteredIndices.size(), LVSICF_NOINVALIDATEALL | LVSICF_NOSCROLL);
|
m_CList_Online.SetItemCountExV((int)m_FilteredIndices.size(), LVSICF_NOINVALIDATEALL | LVSICF_NOSCROLL);
|
||||||
}
|
}
|
||||||
@@ -3785,6 +3908,12 @@ void CMy2015RemoteDlg::Release()
|
|||||||
WebService().Stop();
|
WebService().Stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop MCP service
|
||||||
|
if (McpServer().IsRunning()) {
|
||||||
|
Mprintf("Stopping McpServer...\n");
|
||||||
|
McpServer().Stop();
|
||||||
|
}
|
||||||
|
|
||||||
UninitFileUpload();
|
UninitFileUpload();
|
||||||
DeletePopupWindow(TRUE);
|
DeletePopupWindow(TRUE);
|
||||||
CloseAllLoopTips(); // 关闭所有"播放快照"循环窗口
|
CloseAllLoopTips(); // 关闭所有"播放快照"循环窗口
|
||||||
@@ -3867,8 +3996,6 @@ void CMy2015RemoteDlg::SortByColumn(int nColumn)
|
|||||||
// 缩略图列点击表头不参与排序 —— 没有可比较的数据维度,避免误触发
|
// 缩略图列点击表头不参与排序 —— 没有可比较的数据维度,避免误触发
|
||||||
if (ColumnToDataSlot(nColumn) < 0) return;
|
if (ColumnToDataSlot(nColumn) < 0) return;
|
||||||
|
|
||||||
static int m_nSortColumn = 0;
|
|
||||||
static bool m_bSortAscending = false;
|
|
||||||
if (nColumn == m_nSortColumn) {
|
if (nColumn == m_nSortColumn) {
|
||||||
// 如果点击的是同一列,切换排序顺序
|
// 如果点击的是同一列,切换排序顺序
|
||||||
m_bSortAscending = !m_bSortAscending;
|
m_bSortAscending = !m_bSortAscending;
|
||||||
@@ -3878,8 +4005,25 @@ void CMy2015RemoteDlg::SortByColumn(int nColumn)
|
|||||||
m_bSortAscending = true;
|
m_bSortAscending = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 虚拟列表:对数据源进行排序(列号映射到底层数据槽)
|
|
||||||
EnterCriticalSection(&m_cs);
|
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);
|
int slot = ColumnToDataSlot(m_nSortColumn);
|
||||||
bool asc = m_bSortAscending;
|
bool asc = m_bSortAscending;
|
||||||
std::sort(m_HostList.begin(), m_HostList.end(),
|
std::sort(m_HostList.begin(), m_HostList.end(),
|
||||||
@@ -3890,19 +4034,38 @@ void CMy2015RemoteDlg::SortByColumn(int nColumn)
|
|||||||
return asc ? (result < 0) : (result > 0);
|
return asc ? (result < 0) : (result > 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 重建索引映射
|
// 排序后 clientID -> 索引映射已改变,需重建
|
||||||
m_ClientIndex.clear();
|
m_ClientIndex.clear();
|
||||||
for (size_t i = 0; i < m_HostList.size(); ++i) {
|
for (size_t i = 0; i < m_HostList.size(); ++i) {
|
||||||
if (m_HostList[i]) {
|
if (m_HostList[i]) {
|
||||||
m_ClientIndex[m_HostList[i]->GetClientID()] = i;
|
m_ClientIndex[m_HostList[i]->GetClientID()] = i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 重建过滤索引
|
}
|
||||||
RebuildFilteredIndices();
|
|
||||||
LeaveCriticalSection(&m_cs);
|
|
||||||
|
|
||||||
// 刷新列表
|
void CMy2015RemoteDlg::LoadSortPreference()
|
||||||
m_CList_Online.Invalidate();
|
{
|
||||||
|
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)
|
void CMy2015RemoteDlg::OnHdnItemclickList(NMHDR* pNMHDR, LRESULT* pResult)
|
||||||
@@ -4109,6 +4272,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_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_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_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("发送授权"));
|
Menu.ModifyMenuL(ID_ONLINE_AUTHORIZE, MF_BYCOMMAND | MF_STRING, ID_ONLINE_AUTHORIZE, _T("发送授权"));
|
||||||
|
|
||||||
@@ -4200,6 +4365,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;
|
CMenu newMenu;
|
||||||
if (!newMenu.CreatePopupMenu()) {
|
if (!newMenu.CreatePopupMenu()) {
|
||||||
@@ -5295,6 +5467,15 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
|||||||
// 【x】:对话框相关功能
|
// 【x】:对话框相关功能
|
||||||
switch (cmd) {
|
switch (cmd) {
|
||||||
case TOKEN_REPORT_LOG: {
|
case TOKEN_REPORT_LOG: {
|
||||||
|
// P3:MCP 挂起时接管。客户端子连接建立即回全量日志、随后每 3s 推增量;MCP
|
||||||
|
// 只取首条全量,取走即 CancelIO 关子链接,停止后续增量推送(与 MFC 日志对话框
|
||||||
|
// 持续接收增量的路径区分开)。
|
||||||
|
uint64_t devId = ContextObject->GetClientID();
|
||||||
|
if (McpServer().IsPending(devId)) {
|
||||||
|
McpServer().TakeMainResponse(devId, szBuffer, len);
|
||||||
|
ContextObject->CancelIO();
|
||||||
|
break;
|
||||||
|
}
|
||||||
std::string logText((char*)(szBuffer + 1), len > 1 ? len - 1 : 0);
|
std::string logText((char*)(szBuffer + 1), len > 1 ? len - 1 : 0);
|
||||||
if (!ContextObject->hDlg) {
|
if (!ContextObject->hDlg) {
|
||||||
// 对话框尚未打开:用 SendMessage 同步打开并显示初始全量日志
|
// 对话框尚未打开:用 SendMessage 同步打开并显示初始全量日志
|
||||||
@@ -5308,6 +5489,23 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case TOKEN_REPORT_ACTIVITY: {
|
||||||
|
// P2b:MCP 挂起时接管。历史活动为「主连接 RPC」(无子链接),响应回主 context,
|
||||||
|
// 不能 CancelIO(会断开主连接),仅同步拷贝后 break。
|
||||||
|
uint64_t devId = ContextObject->GetClientID();
|
||||||
|
if (McpServer().IsPending(devId)) {
|
||||||
|
McpServer().TakeMainResponse(devId, ContextObject->GetBuffer(0), ContextObject->GetBufferLength());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// 一次性快照:直接打开“历史活动”对话框展示,不占用 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: {
|
case TOKEN_CLIENT_MSG: {
|
||||||
ClientMsg *msg =(ClientMsg*)ContextObject->InDeCompressedBuffer.GetBuffer(0);
|
ClientMsg *msg =(ClientMsg*)ContextObject->InDeCompressedBuffer.GetBuffer(0);
|
||||||
PostMessageA(WM_SHOWERRORMSG, (WPARAM)new CString(_L(msg->text)), (LPARAM)new CString(_L(msg->title)));
|
PostMessageA(WM_SHOWERRORMSG, (WPARAM)new CString(_L(msg->text)), (LPARAM)new CString(_L(msg->title)));
|
||||||
@@ -6001,6 +6199,12 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
|||||||
// (IO 线程不接触 UI;clientId 用于循环快照路由到对应窗口)。
|
// (IO 线程不接触 UI;clientId 用于循环快照路由到对应窗口)。
|
||||||
// 不用 WPARAM 携带 ID:32 位 Windows 上 WPARAM 是 32 位,会截 64 位 clientID。
|
// 不用 WPARAM 携带 ID:32 位 Windows 上 WPARAM 是 32 位,会截 64 位 clientID。
|
||||||
if (len < sizeof(ScreenPreviewRspHeader)) break;
|
if (len < sizeof(ScreenPreviewRspHeader)) break;
|
||||||
|
// P2c:MCP 挂起时接管(屏幕预览 = 主连接 RPC,不能 CancelIO)。
|
||||||
|
// reqId 关联:命中 MCP 的 expectedReqId 才取走,避免误吞 MFC 预览在途响应。
|
||||||
|
uint64_t previewDevId = ContextObject->GetClientID();
|
||||||
|
const ScreenPreviewRspHeader* previewHdr = reinterpret_cast<const ScreenPreviewRspHeader*>(szBuffer);
|
||||||
|
if (McpServer().TakePreviewResponse(previewDevId, previewHdr->reqId, szBuffer, len))
|
||||||
|
break;
|
||||||
auto* msg = new CMy2015RemoteDlg::PreviewRspMsg{
|
auto* msg = new CMy2015RemoteDlg::PreviewRspMsg{
|
||||||
ContextObject->GetClientID(),
|
ContextObject->GetClientID(),
|
||||||
std::vector<BYTE>(szBuffer, szBuffer + len)
|
std::vector<BYTE>(szBuffer, szBuffer + len)
|
||||||
@@ -6127,10 +6331,29 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case TOKEN_DRIVE_LIST: { // 文件管理【x】
|
case TOKEN_DRIVE_LIST: { // 文件管理【x】
|
||||||
|
// P2c:MCP 挂起时接管。列盘 = 取走数据后用完即关;列目录 = 转向下发
|
||||||
|
// COMMAND_LIST_FILES、等 TOKEN_FILE_LIST(此时不关子链接,交给 TOKEN_FILE_LIST 分支收尾)。
|
||||||
|
uint64_t devId = ContextObject->GetClientID();
|
||||||
|
if (McpServer().IsPending(devId)) {
|
||||||
|
if (McpServer().OnDriveList(devId, ContextObject, szBuffer, len))
|
||||||
|
ContextObject->CancelIO(); // 只列盘 → 用完即关一次性子链接
|
||||||
|
break;
|
||||||
|
}
|
||||||
ContextObject->EnableZstdContext(6);
|
ContextObject->EnableZstdContext(6);
|
||||||
g_2015RemoteDlg->SendMessage(WM_OPENFILEMANAGERDIALOG, 0, (LPARAM)ContextObject);
|
g_2015RemoteDlg->SendMessage(WM_OPENFILEMANAGERDIALOG, 0, (LPARAM)ContextObject);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case TOKEN_FILE_LIST: { // 文件列表【P2c:仅 MCP 无头路径会到达】
|
||||||
|
// 正常文件管理由 FileManagerDlg 的 hDlg 接管,TOKEN_FILE_LIST 不进此分派;
|
||||||
|
// 到达此处只有 MCP 无头列目录这一种情形。挂起命中则取数据,否则(超时迟到)
|
||||||
|
// 直接关掉孤儿子链接,避免泄漏。
|
||||||
|
uint64_t devId = ContextObject->GetClientID();
|
||||||
|
if (McpServer().IsPending(devId)) {
|
||||||
|
McpServer().TakeMainResponse(devId, ContextObject->GetBuffer(0), ContextObject->GetBufferLength());
|
||||||
|
}
|
||||||
|
ContextObject->CancelIO(); // 无论是否命中,用完即关(无 MFC 对话框接管此子链接)
|
||||||
|
break;
|
||||||
|
}
|
||||||
case TOKEN_TALK_START: { // 发送消息【x】
|
case TOKEN_TALK_START: { // 发送消息【x】
|
||||||
g_2015RemoteDlg->SendMessage(WM_OPENTALKDIALOG, 0, (LPARAM)ContextObject);
|
g_2015RemoteDlg->SendMessage(WM_OPENTALKDIALOG, 0, (LPARAM)ContextObject);
|
||||||
break;
|
break;
|
||||||
@@ -6177,6 +6400,14 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
|||||||
}
|
}
|
||||||
case TOKEN_WSLIST: // 窗口管理【x】
|
case TOKEN_WSLIST: // 窗口管理【x】
|
||||||
case TOKEN_PSLIST: { // 进程管理【x】
|
case TOKEN_PSLIST: { // 进程管理【x】
|
||||||
|
// P2b:MCP 挂起时接管。进程/窗口列表无请求 id,按 device_id 匹配并同步拷贝
|
||||||
|
// (此时 IO 线程阻塞在 NotifyProc,缓冲尚未被覆盖,拷贝安全)。
|
||||||
|
uint64_t devId = ContextObject->GetClientID();
|
||||||
|
if (McpServer().IsPending(devId)) {
|
||||||
|
McpServer().TakeMainResponse(devId, ContextObject->GetBuffer(0), ContextObject->GetBufferLength());
|
||||||
|
ContextObject->CancelIO(); // 用完即关,避免子链接泄漏(仿 WebService 终端关闭)
|
||||||
|
break;
|
||||||
|
}
|
||||||
g_2015RemoteDlg->SendMessage(WM_OPENSYSTEMDIALOG, 0, (LPARAM)ContextObject);
|
g_2015RemoteDlg->SendMessage(WM_OPENSYSTEMDIALOG, 0, (LPARAM)ContextObject);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -6189,6 +6420,14 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case TOKEN_SERVERLIST: { // 服务管理【x】
|
case TOKEN_SERVERLIST: { // 服务管理【x】
|
||||||
|
// P3:MCP 挂起时接管。服务列表为一次性子链接回传(CServicesManager 构造即发),
|
||||||
|
// 取走即 CancelIO 关子链接(无 MFC 对话框续用该子链接)。
|
||||||
|
uint64_t devId = ContextObject->GetClientID();
|
||||||
|
if (McpServer().IsPending(devId)) {
|
||||||
|
McpServer().TakeMainResponse(devId, szBuffer, len);
|
||||||
|
ContextObject->CancelIO();
|
||||||
|
break;
|
||||||
|
}
|
||||||
g_2015RemoteDlg->SendMessage(WM_OPENSERVICESDIALOG, 0, (LPARAM)ContextObject);
|
g_2015RemoteDlg->SendMessage(WM_OPENSERVICESDIALOG, 0, (LPARAM)ContextObject);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -9293,11 +9532,22 @@ 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("上线提醒"));
|
||||||
|
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) {
|
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);
|
||||||
|
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) {
|
if (m_CList_Message.GetItemCount() == 0) {
|
||||||
@@ -9369,6 +9619,306 @@ 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::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()
|
void CMy2015RemoteDlg::OnOnlineAddWatch()
|
||||||
{
|
{
|
||||||
EnterCriticalSection(&m_cs);
|
EnterCriticalSection(&m_cs);
|
||||||
@@ -10372,6 +10922,16 @@ void CMy2015RemoteDlg::OnToolPluginSettings()
|
|||||||
dlg.DoModal();
|
dlg.DoModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CMy2015RemoteDlg::OnMcpSettings()
|
||||||
|
{
|
||||||
|
CMcpSettingsDlg dlg(this);
|
||||||
|
if (dlg.DoModal() != IDOK) return;
|
||||||
|
|
||||||
|
// 保存后重生成 frpc.ini:若已启用 MCP 且绑定 0.0.0.0,则把 MCP 端口纳入 FRP 暴露列表
|
||||||
|
// (与监听/Web 端口取并集,互不覆盖)。MCP 生效仍需重启,此处仅为下次启动准备配置。
|
||||||
|
ApplyFrpSettings();
|
||||||
|
}
|
||||||
|
|
||||||
void CMy2015RemoteDlg::OnTriggerSettings()
|
void CMy2015RemoteDlg::OnTriggerSettings()
|
||||||
{
|
{
|
||||||
if (m_DllList.empty()) {
|
if (m_DllList.empty()) {
|
||||||
@@ -11655,6 +12215,12 @@ void CMy2015RemoteDlg::OnOnlineClientLog()
|
|||||||
SendSelectedCommand(&cmd, sizeof(BYTE));
|
SendSelectedCommand(&cmd, sizeof(BYTE));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CMy2015RemoteDlg::OnOnlineHistoryActivity()
|
||||||
|
{
|
||||||
|
BYTE cmd = COMMAND_QUERY_ACTIVITY;
|
||||||
|
SendSelectedCommand(&cmd, sizeof(BYTE));
|
||||||
|
}
|
||||||
|
|
||||||
LRESULT CMy2015RemoteDlg::OnOpenClientLogDialog(WPARAM wParam, LPARAM lParam)
|
LRESULT CMy2015RemoteDlg::OnOpenClientLogDialog(WPARAM wParam, LPARAM lParam)
|
||||||
{
|
{
|
||||||
std::string* pInitLog = reinterpret_cast<std::string*>(wParam);
|
std::string* pInitLog = reinterpret_cast<std::string*>(wParam);
|
||||||
|
|||||||
@@ -173,6 +173,9 @@ protected:
|
|||||||
DECLARE_MESSAGE_MAP()
|
DECLARE_MESSAGE_MAP()
|
||||||
public:
|
public:
|
||||||
void SortByColumn(int nColumn);
|
void SortByColumn(int nColumn);
|
||||||
|
void ApplySort(); // 按偏好重排 m_HostList(调用方需持有 m_cs)
|
||||||
|
void LoadSortPreference();
|
||||||
|
void SaveSortPreference();
|
||||||
afx_msg VOID OnHdnItemclickList(NMHDR* pNMHDR, LRESULT* pResult);
|
afx_msg VOID OnHdnItemclickList(NMHDR* pNMHDR, LRESULT* pResult);
|
||||||
static int CALLBACK CompareFunction(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort);
|
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)
|
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::vector<context*> m_HostList; // 虚拟列表数据源(全部客户端)
|
||||||
std::unordered_map<uint64_t, size_t> m_ClientIndex; // clientID -> m_HostList 索引映射
|
std::unordered_map<uint64_t, size_t> m_ClientIndex; // clientID -> m_HostList 索引映射
|
||||||
std::vector<size_t> m_FilteredIndices; // 当前分组过滤后的索引列表
|
std::vector<size_t> m_FilteredIndices; // 当前分组过滤后的索引列表
|
||||||
|
int m_nSortColumn = -1; // 当前排序列,-1 表示未排序
|
||||||
|
bool m_bSortAscending = true; // 排序方向(true=升序)
|
||||||
std::set<std::string> m_GroupList;
|
std::set<std::string> m_GroupList;
|
||||||
std::string m_selectedGroup;
|
std::string m_selectedGroup;
|
||||||
std::string m_v2KeyPath; // V2 密钥文件路径
|
std::string m_v2KeyPath; // V2 密钥文件路径
|
||||||
@@ -385,7 +390,7 @@ public:
|
|||||||
bool IsDllRequestLimited(const std::string& ip);
|
bool IsDllRequestLimited(const std::string& ip);
|
||||||
void RecordDllRequest(const std::string& ip);
|
void RecordDllRequest(const std::string& ip);
|
||||||
CMenu m_MainMenu;
|
CMenu m_MainMenu;
|
||||||
CBitmap m_bmOnline[63];
|
CBitmap m_bmOnline[66];
|
||||||
uint64_t m_superID;
|
uint64_t m_superID;
|
||||||
std::map<HWND, CDialogBase *> m_RemoteWnds;
|
std::map<HWND, CDialogBase *> m_RemoteWnds;
|
||||||
FileTransformCmd m_CmdList;
|
FileTransformCmd m_CmdList;
|
||||||
@@ -564,6 +569,9 @@ 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 OnMsglogBanIP();
|
||||||
|
afx_msg void OnMsglogUnbanIP();
|
||||||
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();
|
||||||
@@ -630,6 +638,7 @@ public:
|
|||||||
afx_msg void OnMasterTrail();
|
afx_msg void OnMasterTrail();
|
||||||
afx_msg void OnCancelShare();
|
afx_msg void OnCancelShare();
|
||||||
afx_msg void OnWebRemoteControl();
|
afx_msg void OnWebRemoteControl();
|
||||||
|
afx_msg void OnMcpSettings();
|
||||||
afx_msg void OnProxyPortAutorun();
|
afx_msg void OnProxyPortAutorun();
|
||||||
afx_msg void OnScreenpreviewLoop();
|
afx_msg void OnScreenpreviewLoop();
|
||||||
afx_msg void OnMenuCompress();
|
afx_msg void OnMenuCompress();
|
||||||
@@ -642,6 +651,7 @@ public:
|
|||||||
afx_msg void OnOnlineActiveWnd();
|
afx_msg void OnOnlineActiveWnd();
|
||||||
afx_msg void OnEnableDevDebug();
|
afx_msg void OnEnableDevDebug();
|
||||||
afx_msg void OnOnlineClientLog();
|
afx_msg void OnOnlineClientLog();
|
||||||
|
afx_msg void OnOnlineHistoryActivity();
|
||||||
afx_msg LRESULT OnOpenClientLogDialog(WPARAM wParam, LPARAM lParam);
|
afx_msg LRESULT OnOpenClientLogDialog(WPARAM wParam, LPARAM lParam);
|
||||||
afx_msg void OnOnlineForbidden();
|
afx_msg void OnOnlineForbidden();
|
||||||
};
|
};
|
||||||
@@ -291,6 +291,7 @@
|
|||||||
<ClInclude Include="BuildDlg.h" />
|
<ClInclude Include="BuildDlg.h" />
|
||||||
<ClInclude Include="CClientListDlg.h" />
|
<ClInclude Include="CClientListDlg.h" />
|
||||||
<ClInclude Include="CClientLog.h" />
|
<ClInclude Include="CClientLog.h" />
|
||||||
|
<ClInclude Include="CActivityDialog.h" />
|
||||||
<ClInclude Include="LogSearchBar.h" />
|
<ClInclude Include="LogSearchBar.h" />
|
||||||
<ClInclude Include="CDlgFileSend.h" />
|
<ClInclude Include="CDlgFileSend.h" />
|
||||||
<ClInclude Include="CDrawingBoard.h" />
|
<ClInclude Include="CDrawingBoard.h" />
|
||||||
@@ -316,6 +317,7 @@
|
|||||||
<ClInclude Include="file\CFileTransferModeDlg.h" />
|
<ClInclude Include="file\CFileTransferModeDlg.h" />
|
||||||
<ClInclude Include="HideScreenSpyDlg.h" />
|
<ClInclude Include="HideScreenSpyDlg.h" />
|
||||||
<ClInclude Include="HostInfo.h" />
|
<ClInclude Include="HostInfo.h" />
|
||||||
|
<ClInclude Include="HostJson.h" />
|
||||||
<ClInclude Include="InputDlg.h" />
|
<ClInclude Include="InputDlg.h" />
|
||||||
<ClInclude Include="ZstaPickerDlg.h" />
|
<ClInclude Include="ZstaPickerDlg.h" />
|
||||||
<ClInclude Include="IOCPKCPServer.h" />
|
<ClInclude Include="IOCPKCPServer.h" />
|
||||||
@@ -324,6 +326,8 @@
|
|||||||
<ClInclude Include="IPHistoryDlg.h" />
|
<ClInclude Include="IPHistoryDlg.h" />
|
||||||
<ClInclude Include="KeyBoardDlg.h" />
|
<ClInclude Include="KeyBoardDlg.h" />
|
||||||
<ClInclude Include="LangManager.h" />
|
<ClInclude Include="LangManager.h" />
|
||||||
|
<ClInclude Include="McpServer.h" />
|
||||||
|
<ClInclude Include="McpSettingsDlg.h" />
|
||||||
<ClInclude Include="NetworkDlg.h" />
|
<ClInclude Include="NetworkDlg.h" />
|
||||||
<ClInclude Include="NotifyConfig.h" />
|
<ClInclude Include="NotifyConfig.h" />
|
||||||
<ClInclude Include="NotifyManager.h" />
|
<ClInclude Include="NotifyManager.h" />
|
||||||
@@ -374,6 +378,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClCompile Include="..\..\client\Audio.cpp" />
|
<ClCompile Include="..\..\client\Audio.cpp" />
|
||||||
<ClCompile Include="CClientLog.cpp" />
|
<ClCompile Include="CClientLog.cpp" />
|
||||||
|
<ClCompile Include="CActivityDialog.cpp" />
|
||||||
<ClCompile Include="LogSearchBar.cpp" />
|
<ClCompile Include="LogSearchBar.cpp" />
|
||||||
<ClCompile Include="msvc_compat.c">
|
<ClCompile Include="msvc_compat.c">
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
|
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
|
||||||
@@ -385,6 +390,9 @@
|
|||||||
<ClCompile Include="PreviewTipWnd.cpp" />
|
<ClCompile Include="PreviewTipWnd.cpp" />
|
||||||
<ClCompile Include="TriggerSettingsDlg.cpp" />
|
<ClCompile Include="TriggerSettingsDlg.cpp" />
|
||||||
<ClCompile Include="WebService.cpp" />
|
<ClCompile Include="WebService.cpp" />
|
||||||
|
<ClCompile Include="HostJson.cpp" />
|
||||||
|
<ClCompile Include="McpServer.cpp" />
|
||||||
|
<ClCompile Include="McpSettingsDlg.cpp" />
|
||||||
<ClCompile Include="..\..\client\MemoryModule.c">
|
<ClCompile Include="..\..\client\MemoryModule.c">
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||||
|
|||||||
@@ -84,6 +84,9 @@
|
|||||||
<ClCompile Include="PluginSettingsDlg.cpp" />
|
<ClCompile Include="PluginSettingsDlg.cpp" />
|
||||||
<ClCompile Include="PreviewTipWnd.cpp" />
|
<ClCompile Include="PreviewTipWnd.cpp" />
|
||||||
<ClCompile Include="TriggerSettingsDlg.cpp" />
|
<ClCompile Include="TriggerSettingsDlg.cpp" />
|
||||||
|
<ClCompile Include="HostJson.cpp" />
|
||||||
|
<ClCompile Include="McpServer.cpp" />
|
||||||
|
<ClCompile Include="McpSettingsDlg.cpp" />
|
||||||
<ClCompile Include="CClientLog.cpp" />
|
<ClCompile Include="CClientLog.cpp" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -164,6 +167,7 @@
|
|||||||
<ClInclude Include="CDlgFileSend.h" />
|
<ClInclude Include="CDlgFileSend.h" />
|
||||||
<ClInclude Include="..\..\client\reg_startup.h" />
|
<ClInclude Include="..\..\client\reg_startup.h" />
|
||||||
<ClInclude Include="HostInfo.h" />
|
<ClInclude Include="HostInfo.h" />
|
||||||
|
<ClInclude Include="HostJson.h" />
|
||||||
<ClInclude Include="CClientListDlg.h" />
|
<ClInclude Include="CClientListDlg.h" />
|
||||||
<ClInclude Include="context.h" />
|
<ClInclude Include="context.h" />
|
||||||
<ClInclude Include="CUpdateDlg.h" />
|
<ClInclude Include="CUpdateDlg.h" />
|
||||||
@@ -189,6 +193,8 @@
|
|||||||
<ClInclude Include="PluginSettingsDlg.h" />
|
<ClInclude Include="PluginSettingsDlg.h" />
|
||||||
<ClInclude Include="PreviewTipWnd.h" />
|
<ClInclude Include="PreviewTipWnd.h" />
|
||||||
<ClInclude Include="TriggerSettingsDlg.h" />
|
<ClInclude Include="TriggerSettingsDlg.h" />
|
||||||
|
<ClInclude Include="McpServer.h" />
|
||||||
|
<ClInclude Include="McpSettingsDlg.h" />
|
||||||
<ClInclude Include="CClientLog.h" />
|
<ClInclude Include="CClientLog.h" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
// 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);
|
||||||
|
// 解除 EDIT 控件默认约 64KB 长度上限,否则大快照经 SetWindowTextW 会被静默截断。
|
||||||
|
m_editLog.SetLimitText(0);
|
||||||
|
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());
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
};
|
||||||
@@ -37,9 +37,9 @@ BOOL CClientLog::OnInitDialog()
|
|||||||
HICON hIcon = AfxGetApp()->LoadIcon(IDI_CLIENTLOG);
|
HICON hIcon = AfxGetApp()->LoadIcon(IDI_CLIENTLOG);
|
||||||
SetIcon(hIcon, TRUE);
|
SetIcon(hIcon, TRUE);
|
||||||
SetIcon(hIcon, FALSE);
|
SetIcon(hIcon, FALSE);
|
||||||
CFont* pFont = new CFont();
|
m_font.CreatePointFont(85, _T("Consolas"));
|
||||||
pFont->CreatePointFont(85, _T("Consolas"));
|
m_editLog.SetFont(&m_font);
|
||||||
m_editLog.SetFont(pFont);
|
m_editLog.SetLimitText(0); // 解除 EDIT 控件默认长度上限,改由 AppendLog 的 512KB 逻辑管理
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ protected:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
CEdit m_editLog;
|
CEdit m_editLog;
|
||||||
|
CFont m_font;
|
||||||
|
|
||||||
#ifdef AFX_DESIGN_TIME
|
#ifdef AFX_DESIGN_TIME
|
||||||
enum { IDD = IDD_DIALOG_CLIENT_LOG };
|
enum { IDD = IDD_DIALOG_CLIENT_LOG };
|
||||||
|
|||||||
@@ -88,7 +88,9 @@ typedef struct FeatureFlags {
|
|||||||
#define MF_MASTER_TRAIL (1ULL << 41) // HIDE_MENU_MASTER_TRAIL
|
#define MF_MASTER_TRAIL (1ULL << 41) // HIDE_MENU_MASTER_TRAIL
|
||||||
#define MF_REQUEST_AUTH (1ULL << 42) // HIDE_MENU_REQUEST_AUTH
|
#define MF_REQUEST_AUTH (1ULL << 42) // HIDE_MENU_REQUEST_AUTH
|
||||||
|
|
||||||
// [43-63] Reserved
|
#define MF_MCP_SETTINGS (1ULL << 43) // HIDE_MENU_MCP_SETTINGS
|
||||||
|
|
||||||
|
// [44-63] Reserved
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "InputDlg.h"
|
#include "InputDlg.h"
|
||||||
#include "CTextDlg.h"
|
#include "CTextDlg.h"
|
||||||
#include "HideScreenSpyDlg.h"
|
#include "HideScreenSpyDlg.h"
|
||||||
|
#include "2015RemoteDlg.h"
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
@@ -43,6 +44,8 @@ IMPLEMENT_DYNAMIC(CHideScreenSpyDlg, CDialog)
|
|||||||
bool DirectoryExists(const char* path);
|
bool DirectoryExists(const char* path);
|
||||||
std::string GetScreenShotPath(CWnd* parent, const CString& ip, const CString& filter, const CString& suffix);
|
std::string GetScreenShotPath(CWnd* parent, const CString& ip, const CString& filter, const CString& suffix);
|
||||||
|
|
||||||
|
extern CMy2015RemoteDlg* g_2015RemoteDlg;
|
||||||
|
|
||||||
CHideScreenSpyDlg::CHideScreenSpyDlg(CWnd* pParent, Server* pIOCPServer, ClientContext* pContext)
|
CHideScreenSpyDlg::CHideScreenSpyDlg(CWnd* pParent, Server* pIOCPServer, ClientContext* pContext)
|
||||||
: DialogBase(CHideScreenSpyDlg::IDD, pParent, pIOCPServer, pContext, IDI_SCREENSYP)
|
: DialogBase(CHideScreenSpyDlg::IDD, pParent, pIOCPServer, pContext, IDI_SCREENSYP)
|
||||||
{
|
{
|
||||||
@@ -90,11 +93,33 @@ BEGIN_MESSAGE_MAP(CHideScreenSpyDlg, CDialog)
|
|||||||
ON_WM_PAINT()
|
ON_WM_PAINT()
|
||||||
ON_WM_TIMER()
|
ON_WM_TIMER()
|
||||||
ON_WM_CLOSE()
|
ON_WM_CLOSE()
|
||||||
|
ON_WM_SETCURSOR()
|
||||||
END_MESSAGE_MAP()
|
END_MESSAGE_MAP()
|
||||||
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
// CHideScreenSpyDlg message handlers
|
// CHideScreenSpyDlg message handlers
|
||||||
|
BOOL CHideScreenSpyDlg::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
|
||||||
|
{
|
||||||
|
// 查看模式(未开启远程控制):客户区显示禁止图标;标题栏及系统按钮保持正常箭头,
|
||||||
|
// 便于拖动窗口或通过标题栏右键菜单开启远程控制。
|
||||||
|
// 这里按窗口自身状态判断,避免用窗口类光标(GCLP_HCURSOR)时被同类的其他对话框实例
|
||||||
|
// 共享串扰——第二个对话框切到查看模式后,第一个控制中的对话框也被误显示成禁止图标。
|
||||||
|
if (!m_bIsCtrl && nHitTest == HTCLIENT) {
|
||||||
|
::SetCursor(LoadCursor(NULL, IDC_NO));
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
if (!m_bIsCtrl &&
|
||||||
|
(nHitTest == HTCAPTION || nHitTest == HTSYSMENU || nHitTest == HTMENU ||
|
||||||
|
nHitTest == HTCLOSE || nHitTest == HTMINBUTTON || nHitTest == HTMAXBUTTON ||
|
||||||
|
nHitTest == HTHELP)) {
|
||||||
|
::SetCursor(LoadCursor(NULL, IDC_ARROW));
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
// 控制模式:交父类处理,显示窗口类光标(远程光标形状)
|
||||||
|
return __super::OnSetCursor(pWnd, nHitTest, message);
|
||||||
|
}
|
||||||
|
|
||||||
void CHideScreenSpyDlg::OnClose()
|
void CHideScreenSpyDlg::OnClose()
|
||||||
{
|
{
|
||||||
if (!m_aviFile.IsEmpty()) {
|
if (!m_aviFile.IsEmpty()) {
|
||||||
@@ -153,11 +178,22 @@ bool CHideScreenSpyDlg::SaveSnapshot()
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CString CHideScreenSpyDlg::GetTitleHostName() const
|
||||||
|
{
|
||||||
|
// 标题栏显示的主机名:有备注用备注,否则用 IP
|
||||||
|
if (g_2015RemoteDlg && g_2015RemoteDlg->m_ClientMap) {
|
||||||
|
CString note = g_2015RemoteDlg->m_ClientMap->GetClientMapData(GetClientID(), MAP_NOTE);
|
||||||
|
if (!note.IsEmpty())
|
||||||
|
return note;
|
||||||
|
}
|
||||||
|
return m_IPAddress;
|
||||||
|
}
|
||||||
|
|
||||||
BOOL CHideScreenSpyDlg::OnInitDialog()
|
BOOL CHideScreenSpyDlg::OnInitDialog()
|
||||||
{
|
{
|
||||||
__super::OnInitDialog();
|
__super::OnInitDialog();
|
||||||
CString strString;
|
CString strString;
|
||||||
strString.FormatL("%s - 远程虚拟屏幕 %d×%d", m_IPAddress,
|
strString.FormatL("%s - 远程虚拟屏幕 %d×%d", GetTitleHostName().GetString(),
|
||||||
m_BitmapInfor_Full->bmiHeader.biWidth, m_BitmapInfor_Full->bmiHeader.biHeight);
|
m_BitmapInfor_Full->bmiHeader.biWidth, m_BitmapInfor_Full->bmiHeader.biHeight);
|
||||||
SetWindowText(strString);
|
SetWindowText(strString);
|
||||||
|
|
||||||
@@ -165,7 +201,6 @@ BOOL CHideScreenSpyDlg::OnInitDialog()
|
|||||||
// when the application's main window is not a dialog
|
// when the application's main window is not a dialog
|
||||||
SetIcon(m_hIcon, TRUE); // Set big icon
|
SetIcon(m_hIcon, TRUE); // Set big icon
|
||||||
SetIcon(m_hIcon, FALSE); // Set small icon
|
SetIcon(m_hIcon, FALSE); // Set small icon
|
||||||
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, (LONG_PTR)LoadCursor(NULL, IDC_NO));
|
|
||||||
CMenu* pSysMenu = GetSystemMenu(FALSE);
|
CMenu* pSysMenu = GetSystemMenu(FALSE);
|
||||||
if (pSysMenu != NULL) {
|
if (pSysMenu != NULL) {
|
||||||
pSysMenu->AppendMenuSeparator(MF_SEPARATOR);
|
pSysMenu->AppendMenuSeparator(MF_SEPARATOR);
|
||||||
@@ -214,15 +249,7 @@ BOOL CHideScreenSpyDlg::OnInitDialog()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add extra initialization here
|
// TODO: Add extra initialization here
|
||||||
m_hRemoteCursor = LoadCursor(NULL, IDC_ARROW);
|
|
||||||
ICONINFO CursorInfo;
|
|
||||||
::GetIconInfo(m_hRemoteCursor, &CursorInfo);
|
|
||||||
pSysMenu->CheckMenuItem(IDM_CONTROL, m_bIsCtrl ? MF_CHECKED : MF_UNCHECKED);
|
pSysMenu->CheckMenuItem(IDM_CONTROL, m_bIsCtrl ? MF_CHECKED : MF_UNCHECKED);
|
||||||
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, (LONG_PTR)m_hRemoteCursor);
|
|
||||||
if (CursorInfo.hbmMask != NULL)
|
|
||||||
::DeleteObject(CursorInfo.hbmMask);
|
|
||||||
if (CursorInfo.hbmColor != NULL)
|
|
||||||
::DeleteObject(CursorInfo.hbmColor);
|
|
||||||
// 初始化窗口大小结构
|
// 初始化窗口大小结构
|
||||||
m_hFullDC = ::GetDC(m_hWnd);
|
m_hFullDC = ::GetDC(m_hWnd);
|
||||||
m_hFullMemDC = CreateCompatibleDC(m_hFullDC);
|
m_hFullMemDC = CreateCompatibleDC(m_hFullDC);
|
||||||
@@ -440,11 +467,6 @@ void CHideScreenSpyDlg::OnSysCommand(UINT nID, LPARAM lParam)
|
|||||||
case IDM_CONTROL: {
|
case IDM_CONTROL: {
|
||||||
m_bIsCtrl = !m_bIsCtrl;
|
m_bIsCtrl = !m_bIsCtrl;
|
||||||
pSysMenu->CheckMenuItem(IDM_CONTROL, m_bIsCtrl ? MF_CHECKED : MF_UNCHECKED);
|
pSysMenu->CheckMenuItem(IDM_CONTROL, m_bIsCtrl ? MF_CHECKED : MF_UNCHECKED);
|
||||||
|
|
||||||
if (m_bIsCtrl) {
|
|
||||||
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, (LONG_PTR)m_hRemoteCursor);
|
|
||||||
} else
|
|
||||||
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, (LONG_PTR)LoadCursor(NULL, IDC_NO));
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ public:
|
|||||||
afx_msg void OnPaint();
|
afx_msg void OnPaint();
|
||||||
afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
|
afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
|
||||||
afx_msg void OnSize(UINT nType, int cx, int cy);
|
afx_msg void OnSize(UINT nType, int cx, int cy);
|
||||||
|
afx_msg BOOL OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message);
|
||||||
|
|
||||||
virtual LRESULT WindowProc(UINT message, WPARAM wParam, LPARAM lParam);
|
virtual LRESULT WindowProc(UINT message, WPARAM wParam, LPARAM lParam);
|
||||||
|
|
||||||
@@ -71,11 +72,13 @@ protected:
|
|||||||
bool JPG_BMP(int cbit, void* input, int inlen, void* output);
|
bool JPG_BMP(int cbit, void* input, int inlen, void* output);
|
||||||
void ResetScreen();
|
void ResetScreen();
|
||||||
|
|
||||||
|
// 标题栏显示的主机名:有备注用备注,否则用 IP
|
||||||
|
CString GetTitleHostName() const;
|
||||||
|
|
||||||
HDC m_hFullDC, m_hFullMemDC;
|
HDC m_hFullDC, m_hFullMemDC;
|
||||||
HBITMAP m_BitmapHandle;
|
HBITMAP m_BitmapHandle;
|
||||||
LPVOID m_BitmapData_Full;
|
LPVOID m_BitmapData_Full;
|
||||||
LPBITMAPINFO m_BitmapInfor_Full;
|
LPBITMAPINFO m_BitmapInfor_Full;
|
||||||
HCURSOR m_hRemoteCursor;
|
|
||||||
CCursorInfo m_CursorInfo;
|
CCursorInfo m_CursorInfo;
|
||||||
BOOL m_bIsFirst;
|
BOOL m_bIsFirst;
|
||||||
BOOL m_bIsCtrl;
|
BOOL m_bIsCtrl;
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
#include "stdafx.h"
|
||||||
|
#include "HostJson.h"
|
||||||
|
#include "HostInfo.h" // _ClientList / MAP_NOTE
|
||||||
|
#include "context.h" // context 接口 + ONLINELIST_* 枚举
|
||||||
|
#include "2015RemoteDlg.h" // GetClientEncoding 辅助函数(UTF-8 能力位判断)
|
||||||
|
|
||||||
|
#ifndef _WIN64
|
||||||
|
#ifdef _DEBUG
|
||||||
|
#pragma comment(lib, "jsoncpp/jsoncppd.lib")
|
||||||
|
#else
|
||||||
|
#pragma comment(lib, "jsoncpp/jsoncpp.lib")
|
||||||
|
#endif
|
||||||
|
#else
|
||||||
|
#ifdef _DEBUG
|
||||||
|
#pragma comment(lib, "jsoncpp/jsoncpp_x64d.lib")
|
||||||
|
#else
|
||||||
|
#pragma comment(lib, "jsoncpp/jsoncpp_x64.lib")
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Helper: Convert ANSI (GBK) string to UTF-8
|
||||||
|
// (自 WebService.cpp 抽出,供 BuildDeviceListJson 与 BuildHostJson 共用)
|
||||||
|
static std::string AnsiToUtf8(const CString& str) {
|
||||||
|
if (str.IsEmpty()) return "";
|
||||||
|
#ifdef _UNICODE
|
||||||
|
// Unicode build: CString is already UTF-16, convert to UTF-8
|
||||||
|
int len = WideCharToMultiByte(CP_UTF8, 0, str, -1, NULL, 0, NULL, NULL);
|
||||||
|
if (len <= 0) return "";
|
||||||
|
std::string result(len - 1, '\0');
|
||||||
|
WideCharToMultiByte(CP_UTF8, 0, str, -1, &result[0], len, NULL, NULL);
|
||||||
|
return result;
|
||||||
|
#else
|
||||||
|
// MBCS build: CString is ANSI (GBK), need to convert to UTF-16 first, then to UTF-8
|
||||||
|
int wlen = MultiByteToWideChar(CP_ACP, 0, str, -1, NULL, 0);
|
||||||
|
if (wlen <= 0) return "";
|
||||||
|
std::wstring wstr(wlen - 1, L'\0');
|
||||||
|
MultiByteToWideChar(CP_ACP, 0, str, -1, &wstr[0], wlen);
|
||||||
|
|
||||||
|
int u8len = WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, NULL, 0, NULL, NULL);
|
||||||
|
if (u8len <= 0) return "";
|
||||||
|
std::string result(u8len - 1, '\0');
|
||||||
|
WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, &result[0], u8len, NULL, NULL);
|
||||||
|
return result;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value BuildHostJson(context* ctx, _ClientList* clientMap) {
|
||||||
|
Json::Value device;
|
||||||
|
|
||||||
|
if (!ctx) return device; // 空 context → 返回 null Json::Value
|
||||||
|
|
||||||
|
// Use string for ID to avoid JavaScript number precision loss
|
||||||
|
device["id"] = std::to_string(ctx->GetClientID());
|
||||||
|
|
||||||
|
CString name = ctx->GetClientData(ONLINELIST_COMPUTER_NAME);
|
||||||
|
device["name"] = AnsiToUtf8(name);
|
||||||
|
|
||||||
|
// 用户在 MFC 端给这台主机起的备注(菜单"修改备注"写入 MAP_NOTE)
|
||||||
|
if (clientMap) {
|
||||||
|
CString remark = clientMap->GetClientMapData(ctx->GetClientID(), MAP_NOTE);
|
||||||
|
if (!remark.IsEmpty()) {
|
||||||
|
device["remark"] = AnsiToUtf8(remark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CString ip = ctx->GetClientData(ONLINELIST_IP);
|
||||||
|
device["ip"] = AnsiToUtf8(ip);
|
||||||
|
|
||||||
|
CString os = ctx->GetClientData(ONLINELIST_OS);
|
||||||
|
device["os"] = AnsiToUtf8(os);
|
||||||
|
|
||||||
|
CString location = ctx->GetClientData(ONLINELIST_LOCATION);
|
||||||
|
device["location"] = AnsiToUtf8(location);
|
||||||
|
|
||||||
|
CString rtt = ctx->GetClientData(ONLINELIST_PING);
|
||||||
|
device["rtt"] = AnsiToUtf8(rtt);
|
||||||
|
|
||||||
|
CString version = ctx->GetClientData(ONLINELIST_VERSION);
|
||||||
|
device["version"] = AnsiToUtf8(version);
|
||||||
|
|
||||||
|
// 活动窗口编码由客户端能力位决定:新客户端是 UTF-8,老客户端是 CP_ACP(默认 936)。
|
||||||
|
// 不能像其它字段那样无脑 AnsiToUtf8——会把新客户端的 UTF-8 字节再当 GBK 双重编码。
|
||||||
|
CString activeWindow = ctx->GetClientData(ONLINELIST_LOGINTIME);
|
||||||
|
std::string activeWindowU8;
|
||||||
|
if (!activeWindow.IsEmpty()) {
|
||||||
|
UINT cp = GetClientEncoding(ctx);
|
||||||
|
int wlen = MultiByteToWideChar(cp, 0, activeWindow, -1, NULL, 0);
|
||||||
|
if (wlen > 1) {
|
||||||
|
std::wstring w(wlen - 1, L'\0');
|
||||||
|
MultiByteToWideChar(cp, 0, activeWindow, -1, &w[0], wlen);
|
||||||
|
int u8len = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, NULL, 0, NULL, NULL);
|
||||||
|
if (u8len > 1) {
|
||||||
|
activeWindowU8.resize(u8len - 1);
|
||||||
|
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, &activeWindowU8[0], u8len, NULL, NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
device["activeWindow"] = activeWindowU8;
|
||||||
|
device["online"] = true;
|
||||||
|
|
||||||
|
// Add device group to response
|
||||||
|
std::string deviceGroup = ctx->GetGroupName();
|
||||||
|
if (deviceGroup.empty()) deviceGroup = "default";
|
||||||
|
device["group"] = deviceGroup;
|
||||||
|
|
||||||
|
// Get screen info from client's reported resolution
|
||||||
|
// Format: "n:MxN" where n=monitor count, M=width, N=height
|
||||||
|
CString resolution = ctx->GetAdditionalData(RES_RESOLUTION);
|
||||||
|
if (!resolution.IsEmpty()) {
|
||||||
|
device["screen"] = AnsiToUtf8(resolution); // e.g. "2:3840x1080"
|
||||||
|
}
|
||||||
|
|
||||||
|
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
|
||||||
|
device["clientType"] = AnsiToUtf8(clientType); // e.g. "MAC", "LNX", "EXE"
|
||||||
|
|
||||||
|
return device;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "jsoncpp/json.h"
|
||||||
|
|
||||||
|
class context;
|
||||||
|
class _ClientList;
|
||||||
|
|
||||||
|
// 公共函数:将单台在线主机 context 序列化为 JSON(UTF-8,含 GBK/UTF-8 编码处理)。
|
||||||
|
// 供 Web UI(CWebService::BuildDeviceListJson)与 MCP 工具(list_online_hosts)复用,
|
||||||
|
// 避免两处重复易错的编码转换(见 docs/Mcp_Design.md §3.4 方案 C)。
|
||||||
|
//
|
||||||
|
// 职责边界:本函数只负责「单台主机 → Json::Value」的字段序列化与编码转换。
|
||||||
|
// 分组/用户过滤、m_cs 加锁、外层结构(Web 的 {"cmd","devices"} vs MCP 的 {"hosts"})
|
||||||
|
// 属于调用方,不进本函数。调用方须在 m_cs 锁内调用。
|
||||||
|
Json::Value BuildHostJson(context* ctx, _ClientList* clientMap);
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,121 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Windows.h>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <mutex>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <map>
|
||||||
|
#include <vector>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
// httplib 与 Windows 头部的 min/max 宏冲突,按 file_server.h 的既有约定处理。
|
||||||
|
#undef min
|
||||||
|
#undef max
|
||||||
|
#include "httplib.h"
|
||||||
|
#ifndef max
|
||||||
|
#define max(a,b) (((a) > (b)) ? (a) : (b))
|
||||||
|
#endif
|
||||||
|
#ifndef min
|
||||||
|
#define min(a,b) (((a) < (b)) ? (a) : (b))
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#pragma comment(lib, "ws2_32.lib")
|
||||||
|
|
||||||
|
class CMy2015RemoteDlg;
|
||||||
|
class context;
|
||||||
|
|
||||||
|
// MCP (Model Context Protocol) 服务端:httplib 封装 + JSON-RPC 2.0 分发 + 静态 token 校验。
|
||||||
|
// 与 CWebService 平级、互不依赖;默认禁用,经「扩展 → MCP设置」开启后监听
|
||||||
|
// (默认 127.0.0.1:6544,仅本机回环)。见 docs/Mcp_Design.md。
|
||||||
|
class CMcpServer {
|
||||||
|
public:
|
||||||
|
static CMcpServer& Instance();
|
||||||
|
|
||||||
|
void SetParentDlg(CMy2015RemoteDlg* pDlg) { m_parent = pDlg; }
|
||||||
|
void SetToken(const std::string& token) { m_token = token; }
|
||||||
|
|
||||||
|
bool Start(const std::string& bind, int port);
|
||||||
|
void Stop();
|
||||||
|
bool IsRunning() const { return m_running.load(); }
|
||||||
|
|
||||||
|
// ===== P2b:无请求 id 的一次性响应 → 每 host 单飞行 =====
|
||||||
|
// 进程/窗口列表(TOKEN_PSLIST/TOKEN_WSLIST,一次性子链接)与历史活动
|
||||||
|
// (TOKEN_REPORT_ACTIVITY,主连接 RPC)都不带请求关联,同一 host 同一时刻
|
||||||
|
// 只允许一个挂起请求,避免同 host 并发请求响应归属歧义。
|
||||||
|
// 注意:单飞行只约束 MCP 侧同 host 并发,不约束与 MFC 对话框并存(各用独立子链接)。
|
||||||
|
|
||||||
|
// 该 host 是否已有挂起请求(MessageHandle 在 TOKEN_PSLIST/TOKEN_WSLIST 分支调用)。
|
||||||
|
bool IsPending(uint64_t device_id);
|
||||||
|
|
||||||
|
// 拷贝响应缓冲到挂起结果并唤醒等待者(在 MessageHandle 内同步调用,数据此时仍在
|
||||||
|
// context 缓冲中、IO 线程被阻塞,拷贝是安全的)。
|
||||||
|
void TakeMainResponse(uint64_t device_id, const BYTE* data, ULONG len);
|
||||||
|
|
||||||
|
// 工具线程:登记挂起(false = 该 host 已有挂起请求,设备忙)。
|
||||||
|
bool BeginPending(uint64_t device_id, const std::string& tool);
|
||||||
|
|
||||||
|
// 工具线程:等待响应;成功返回 true 并把缓冲写入 out,超时/失败返回 false(并清理)。
|
||||||
|
bool WaitPending(uint64_t device_id, std::vector<BYTE>& out, int timeoutMs);
|
||||||
|
|
||||||
|
// 工具线程:清理挂起状态(发送失败等提前退出路径)。
|
||||||
|
void ClearPending(uint64_t device_id);
|
||||||
|
|
||||||
|
// ===== P2c:list_files / get_screenshot 扩展 =====
|
||||||
|
|
||||||
|
// 登记挂起(带目录路径;list_files 专用,path 为空表示只列盘)。
|
||||||
|
bool BeginPending(uint64_t device_id, const std::string& tool, const std::string& path);
|
||||||
|
|
||||||
|
// get_screenshot:工具线程生成 16 位 reqId(跳过 0,0 表示未设置),用于丢弃过期响应。
|
||||||
|
uint16_t NextPreviewReqId();
|
||||||
|
|
||||||
|
// get_screenshot:登记期望的 reqId(必须在发送 COMMAND_SCREEN_PREVIEW_REQ 前调用)。
|
||||||
|
void SetPendingReqId(uint64_t device_id, uint16_t reqId);
|
||||||
|
|
||||||
|
// get_screenshot:MessageHandle(TOKEN_SCREEN_PREVIEW_RSP) 在 IO 线程同步调用;
|
||||||
|
// 挂起命中且 reqId 一致则拷贝响应并返回 true,否则 false(回落到 MFC 预览路径)。
|
||||||
|
bool TakePreviewResponse(uint64_t device_id, uint16_t reqId, const BYTE* data, ULONG len);
|
||||||
|
|
||||||
|
// list_files:MessageHandle(TOKEN_DRIVE_LIST) 调用。
|
||||||
|
// 返回 true = 已取走盘列表(调用方 CancelIO);false = 已转向下发 COMMAND_LIST_FILES、
|
||||||
|
// 等 TOKEN_FILE_LIST(调用方不关子链接)。
|
||||||
|
bool OnDriveList(uint64_t device_id, context* subCtx, const BYTE* buf, ULONG len);
|
||||||
|
|
||||||
|
private:
|
||||||
|
CMcpServer();
|
||||||
|
~CMcpServer();
|
||||||
|
CMcpServer(const CMcpServer&) = delete;
|
||||||
|
CMcpServer& operator=(const CMcpServer&) = delete;
|
||||||
|
|
||||||
|
// POST /mcp 处理器:token 校验 + JSON-RPC 分发。
|
||||||
|
void HandleMcp(const httplib::Request& req, httplib::Response& res);
|
||||||
|
|
||||||
|
httplib::Server m_server;
|
||||||
|
std::thread m_thread;
|
||||||
|
std::atomic<bool> m_running{false};
|
||||||
|
std::string m_token;
|
||||||
|
CMy2015RemoteDlg* m_parent = nullptr;
|
||||||
|
|
||||||
|
// 挂起请求注册表(受 m_PendingMutex 保护,键 = device_id)。
|
||||||
|
struct PendingRequest {
|
||||||
|
std::string tool;
|
||||||
|
std::string path; // list_files:目录路径;空 = 只列盘
|
||||||
|
uint16_t expectedReqId = 0; // get_screenshot:期望的预览 reqId(0 = 未设置)
|
||||||
|
std::vector<BYTE> data;
|
||||||
|
bool done = false;
|
||||||
|
};
|
||||||
|
std::mutex m_PendingMutex;
|
||||||
|
std::condition_variable m_PendingCv;
|
||||||
|
std::map<uint64_t, PendingRequest> m_Pending;
|
||||||
|
|
||||||
|
// get_screenshot 的 reqId 发生器(16 位,跳过 0)。与 MFC 预览的 m_PreviewReqId 各自独立计数。
|
||||||
|
std::atomic<uint16_t> m_PreviewReqId{1};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 全局访问器(仿 WebService(),见 WebService.h 末尾)
|
||||||
|
inline CMcpServer& McpServer() { return CMcpServer::Instance(); }
|
||||||
|
|
||||||
|
// 生成 32 hex(128-bit)随机 token;供运行时兜底与「MCP设置」对话框预填复用。
|
||||||
|
std::string GenerateRandomToken();
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
#include "stdafx.h"
|
||||||
|
#include "McpSettingsDlg.h"
|
||||||
|
#include "LangManager.h" // _TR / MessageBoxL
|
||||||
|
#include "McpServer.h" // GenerateRandomToken
|
||||||
|
#include "2015Remote.h" // THIS_CFG (GetThisCfg)
|
||||||
|
#include <atlconv.h>
|
||||||
|
|
||||||
|
#ifdef _DEBUG
|
||||||
|
#define new DEBUG_NEW
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// 构造一个无控件的 DLGTEMPLATE:cdit=0,控件由 OnInitDialog 动态创建。
|
||||||
|
// (与 ZstaPickerDlg.cpp 内同名函数一致)
|
||||||
|
void BuildDialogTemplate(std::vector<BYTE>& out, LPCWSTR caption, short cx, short cy)
|
||||||
|
{
|
||||||
|
out.clear();
|
||||||
|
auto append = [&](const void* p, size_t n) {
|
||||||
|
const BYTE* b = (const BYTE*)p;
|
||||||
|
out.insert(out.end(), b, b + n);
|
||||||
|
};
|
||||||
|
auto appendW = [&](WORD v) {
|
||||||
|
out.push_back((BYTE)(v & 0xFF));
|
||||||
|
out.push_back((BYTE)((v >> 8) & 0xFF));
|
||||||
|
};
|
||||||
|
auto appendWStr = [&](LPCWSTR s) {
|
||||||
|
size_t n = wcslen(s);
|
||||||
|
append(s, (n + 1) * sizeof(WCHAR));
|
||||||
|
};
|
||||||
|
|
||||||
|
DLGTEMPLATE dt = { 0 };
|
||||||
|
dt.style = DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER |
|
||||||
|
WS_POPUP | WS_CAPTION | WS_SYSMENU;
|
||||||
|
dt.dwExtendedStyle = 0;
|
||||||
|
dt.cdit = 0;
|
||||||
|
dt.x = 0; dt.y = 0;
|
||||||
|
dt.cx = cx; dt.cy = cy;
|
||||||
|
append(&dt, sizeof(dt));
|
||||||
|
appendW(0); // no menu
|
||||||
|
appendW(0); // default dialog class
|
||||||
|
appendWStr(caption); // caption
|
||||||
|
appendW(8); // font point size
|
||||||
|
appendWStr(L"MS Shell Dlg"); // typeface
|
||||||
|
|
||||||
|
while (out.size() % 4) out.push_back(0); // DWORD align
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
BEGIN_MESSAGE_MAP(CMcpSettingsDlg, CDialog)
|
||||||
|
END_MESSAGE_MAP()
|
||||||
|
|
||||||
|
CMcpSettingsDlg::CMcpSettingsDlg(CWnd* parent)
|
||||||
|
: CDialog((LPCTSTR)NULL, parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
INT_PTR CMcpSettingsDlg::DoModal()
|
||||||
|
{
|
||||||
|
USES_CONVERSION;
|
||||||
|
CString title = _TR("MCP设置");
|
||||||
|
BuildDialogTemplate(m_Template, T2CW(title), 320, 150);
|
||||||
|
InitModalIndirect((LPCDLGTEMPLATE)m_Template.data());
|
||||||
|
return CDialog::DoModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL CMcpSettingsDlg::OnInitDialog()
|
||||||
|
{
|
||||||
|
CDialog::OnInitDialog();
|
||||||
|
|
||||||
|
CRect cli;
|
||||||
|
GetClientRect(&cli);
|
||||||
|
|
||||||
|
// 占位 rect,真正布局在 LayoutControls 里
|
||||||
|
CRect r0(0, 0, 10, 10);
|
||||||
|
|
||||||
|
m_btnEnable.Create(_TR("启用 MCP"),
|
||||||
|
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
|
||||||
|
r0, this, IDC_MCP_ENABLE);
|
||||||
|
m_lblPort.Create(_TR("端口"), WS_CHILD | WS_VISIBLE, r0, this, (UINT)-1);
|
||||||
|
m_editPort.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | ES_AUTOHSCROLL,
|
||||||
|
r0, this, IDC_MCP_PORT);
|
||||||
|
m_lblBind.Create(_TR("绑定地址"), WS_CHILD | WS_VISIBLE, r0, this, (UINT)-1);
|
||||||
|
m_editBind.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | ES_AUTOHSCROLL,
|
||||||
|
r0, this, IDC_MCP_BIND);
|
||||||
|
m_lblToken.Create(_TR("Token"), WS_CHILD | WS_VISIBLE, r0, this, (UINT)-1);
|
||||||
|
m_editToken.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | ES_AUTOHSCROLL,
|
||||||
|
r0, this, IDC_MCP_TOKEN);
|
||||||
|
m_btnOK.Create(_TR("确定"), WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON,
|
||||||
|
r0, this, IDOK);
|
||||||
|
m_btnCancel.Create(_TR("取消"), WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON,
|
||||||
|
r0, this, IDCANCEL);
|
||||||
|
|
||||||
|
// 子控件继承对话框字体 (DS_SETFONT)
|
||||||
|
HFONT hFont = (HFONT)::SendMessage(GetSafeHwnd(), WM_GETFONT, 0, 0);
|
||||||
|
if (hFont) {
|
||||||
|
m_btnEnable.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||||
|
m_lblPort.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||||
|
m_editPort.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||||
|
m_lblBind.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||||
|
m_editBind.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||||
|
m_lblToken.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||||
|
m_editToken.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||||
|
m_btnOK.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||||
|
m_btnCancel.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回填配置(首次打开且 token 为空时预填随机值,随保存持久化)
|
||||||
|
int enabled = THIS_CFG.GetInt("settings", "McpEnabled", 0);
|
||||||
|
int port = THIS_CFG.GetInt("settings", "McpPort", 6544);
|
||||||
|
std::string bind = THIS_CFG.GetStr("settings", "McpBind", "127.0.0.1");
|
||||||
|
std::string tok = THIS_CFG.GetStr("settings", "McpToken", "");
|
||||||
|
if (tok.empty()) tok = GenerateRandomToken();
|
||||||
|
|
||||||
|
m_btnEnable.SetCheck(enabled ? BST_CHECKED : BST_UNCHECKED);
|
||||||
|
m_editPort.SetWindowText(CString(std::to_string(port).c_str()));
|
||||||
|
m_editBind.SetWindowText(CString(bind.c_str()));
|
||||||
|
m_editToken.SetWindowText(CString(tok.c_str()));
|
||||||
|
|
||||||
|
LayoutControls(cli.Width(), cli.Height());
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMcpSettingsDlg::OnOK()
|
||||||
|
{
|
||||||
|
CString sPort, sBind, sToken;
|
||||||
|
m_editPort.GetWindowText(sPort);
|
||||||
|
m_editBind.GetWindowText(sBind);
|
||||||
|
m_editToken.GetWindowText(sToken);
|
||||||
|
bool enabled = (m_btnEnable.GetCheck() == BST_CHECKED);
|
||||||
|
|
||||||
|
// 端口校验:1-65535
|
||||||
|
int port = atoi(CT2A(sPort));
|
||||||
|
if (port < 1 || port > 65535) {
|
||||||
|
MessageBoxL(_TR("端口需为 1-65535 的数字"), _TR("提示"), MB_ICONWARNING);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string bind = CT2A(sBind);
|
||||||
|
if (bind.empty()) bind = "127.0.0.1";
|
||||||
|
|
||||||
|
std::string token = CT2A(sToken);
|
||||||
|
if (enabled && token.empty()) {
|
||||||
|
MessageBoxL(_TR("Token 不能为空"), _TR("提示"), MB_ICONWARNING);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 落盘
|
||||||
|
THIS_CFG.SetInt("settings", "McpEnabled", enabled ? 1 : 0);
|
||||||
|
THIS_CFG.SetInt("settings", "McpPort", port);
|
||||||
|
THIS_CFG.SetStr("settings", "McpBind", bind);
|
||||||
|
THIS_CFG.SetStr("settings", "McpToken", token);
|
||||||
|
|
||||||
|
// 拆成两段可翻译的单行键,中间用 \r\n 连接(多行键无法在 INI 中表示)
|
||||||
|
MessageBox(_TR("MCP 设置已保存。") + _T("\r\n") +
|
||||||
|
_TR("启用/端口/绑定地址/Token 的改动需重启程序生效。"),
|
||||||
|
_TR("提示"), MB_ICONINFORMATION);
|
||||||
|
|
||||||
|
CDialog::OnOK();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMcpSettingsDlg::LayoutControls(int cx, int cy)
|
||||||
|
{
|
||||||
|
const int margin = 14;
|
||||||
|
const int labelW = 72;
|
||||||
|
const int rowH = 24;
|
||||||
|
const int gap = 10;
|
||||||
|
const int btnW = 88;
|
||||||
|
const int btnH = 26;
|
||||||
|
|
||||||
|
int y = margin;
|
||||||
|
m_btnEnable.MoveWindow(margin, y, cx - margin * 2, 22);
|
||||||
|
y += 30;
|
||||||
|
|
||||||
|
m_lblPort.MoveWindow(margin, y, labelW, rowH);
|
||||||
|
m_editPort.MoveWindow(margin + labelW, y - 2, 100, rowH);
|
||||||
|
y += rowH + gap;
|
||||||
|
|
||||||
|
m_lblBind.MoveWindow(margin, y, labelW, rowH);
|
||||||
|
m_editBind.MoveWindow(margin + labelW, y - 2, cx - margin * 2 - labelW, rowH);
|
||||||
|
y += rowH + gap;
|
||||||
|
|
||||||
|
m_lblToken.MoveWindow(margin, y, labelW, rowH);
|
||||||
|
m_editToken.MoveWindow(margin + labelW, y - 2, cx - margin * 2 - labelW, rowH);
|
||||||
|
y += rowH + gap;
|
||||||
|
|
||||||
|
int bottomY = cy - margin - btnH;
|
||||||
|
m_btnCancel.MoveWindow(cx - margin - btnW, bottomY, btnW, btnH);
|
||||||
|
m_btnOK.MoveWindow(cx - margin - btnW * 2 - gap, bottomY, btnW, btnH);
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <afxwin.h>
|
||||||
|
#include <afxcmn.h>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// 「MCP设置」配置对话框:启用 MCP / 端口 / 绑定地址 / Token。
|
||||||
|
// 因 2015Remote.rc 为 UTF-16 编码、不宜手工编辑,参照 ZstaPickerDlg 用
|
||||||
|
// InitModalIndirect + 内存 DLGTEMPLATE 程序化创建(见 docs/Mcp_Design.md §3.8)。
|
||||||
|
class CMcpSettingsDlg : public CDialog
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit CMcpSettingsDlg(CWnd* parent = nullptr);
|
||||||
|
|
||||||
|
virtual INT_PTR DoModal();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual BOOL OnInitDialog();
|
||||||
|
virtual void OnOK();
|
||||||
|
|
||||||
|
DECLARE_MESSAGE_MAP()
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum CtrlId {
|
||||||
|
IDC_MCP_ENABLE = 1001, // 「启用 MCP」复选框
|
||||||
|
IDC_MCP_PORT = 1002, // 端口编辑框
|
||||||
|
IDC_MCP_BIND = 1003, // 绑定地址编辑框
|
||||||
|
IDC_MCP_TOKEN = 1004, // Token 编辑框
|
||||||
|
};
|
||||||
|
|
||||||
|
CButton m_btnEnable;
|
||||||
|
CStatic m_lblPort, m_lblBind, m_lblToken;
|
||||||
|
CEdit m_editPort, m_editBind, m_editToken;
|
||||||
|
CButton m_btnOK, m_btnCancel;
|
||||||
|
|
||||||
|
std::vector<BYTE> m_Template; // 内存 DLGTEMPLATE 字节
|
||||||
|
|
||||||
|
void LayoutControls(int cx, int cy);
|
||||||
|
};
|
||||||
@@ -104,10 +104,14 @@ inline const char* GetColumnName(int index) {
|
|||||||
"Camera", // 6
|
"Camera", // 6
|
||||||
"RTT", // 7
|
"RTT", // 7
|
||||||
"Version", // 8
|
"Version", // 8
|
||||||
"Privilege", // 9 ONLINELIST_PRIVILEGE
|
"InstallTime", // 9 ONLINELIST_INSTALLTIME
|
||||||
"InstallTime", // 10 ONLINELIST_INSTALLTIME
|
"ActiveWindow", // 10 ONLINELIST_LOGINTIME
|
||||||
"ActiveWindow", // 11 ONLINELIST_LOGINTIME
|
"ClientType", // 11 ONLINELIST_CLIENTTYPE
|
||||||
"ClientType", // 12 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])) {
|
if (index >= 0 && index < sizeof(names)/sizeof(names[0])) {
|
||||||
return names[index];
|
return names[index];
|
||||||
|
|||||||
@@ -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()) {
|
||||||
// Convert to std::string for matching
|
outMatchedKeyword = matched;
|
||||||
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;
|
m_config.lastNotifyTime[clientId] = now;
|
||||||
return true;
|
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;
|
return false;
|
||||||
|
|||||||
@@ -111,10 +111,10 @@ void NotifySettingsDlg::PopulateComboBoxes()
|
|||||||
{ ONLINELIST_VIDEO, _T("摄像头") },
|
{ ONLINELIST_VIDEO, _T("摄像头") },
|
||||||
{ ONLINELIST_PING, _T("延迟") },
|
{ ONLINELIST_PING, _T("延迟") },
|
||||||
{ ONLINELIST_VERSION, _T("版本") },
|
{ ONLINELIST_VERSION, _T("版本") },
|
||||||
{ ONLINELIST_PRIVILEGE, _T("权限") },
|
|
||||||
{ ONLINELIST_INSTALLTIME, _T("安装时间") },
|
{ ONLINELIST_INSTALLTIME, _T("安装时间") },
|
||||||
{ ONLINELIST_LOGINTIME, _T("活动窗口") },
|
{ ONLINELIST_LOGINTIME, _T("活动窗口") },
|
||||||
{ ONLINELIST_CLIENTTYPE, _T("客户端类型") },
|
{ ONLINELIST_CLIENTTYPE, _T("客户端类型") },
|
||||||
|
{ ONLINELIST_PRIVILEGE, _T("权限") },
|
||||||
};
|
};
|
||||||
for (const auto& col : columns) {
|
for (const auto& col : columns) {
|
||||||
CString item;
|
CString item;
|
||||||
|
|||||||
@@ -648,7 +648,7 @@ void CScreenSpyDlg::PrepareDrawing(const LPBITMAPINFO bmp)
|
|||||||
m_BitmapData_Full = NULL;
|
m_BitmapData_Full = NULL;
|
||||||
|
|
||||||
CString strString;
|
CString strString;
|
||||||
strString.FormatL("%s - 远程桌面控制 %d×%d", m_IPAddress, bmp->bmiHeader.biWidth, bmp->bmiHeader.biHeight);
|
strString.FormatL("%s - 远程桌面控制 %d×%d", GetTitleHostName().GetString(), bmp->bmiHeader.biWidth, bmp->bmiHeader.biHeight);
|
||||||
SetWindowText(strString);
|
SetWindowText(strString);
|
||||||
uint64_t dlg = (uint64_t)this;
|
uint64_t dlg = (uint64_t)this;
|
||||||
Mprintf("%s [对话框ID: %llu]\n", strString.GetString(), dlg);
|
Mprintf("%s [对话框ID: %llu]\n", strString.GetString(), dlg);
|
||||||
@@ -843,13 +843,9 @@ BOOL CScreenSpyDlg::OnInitDialog()
|
|||||||
m_ClientCursorPos.y = 0;
|
m_ClientCursorPos.y = 0;
|
||||||
m_bCursorIndex = 0;
|
m_bCursorIndex = 0;
|
||||||
|
|
||||||
m_hRemoteCursor = LoadCursor(NULL, IDC_ARROW);
|
|
||||||
ICONINFO CursorInfo;
|
|
||||||
::GetIconInfo(m_hRemoteCursor, &CursorInfo);
|
|
||||||
SysMenu->CheckMenuItem(IDM_CONTROL, m_bIsCtrl ? MF_CHECKED : MF_UNCHECKED);
|
SysMenu->CheckMenuItem(IDM_CONTROL, m_bIsCtrl ? MF_CHECKED : MF_UNCHECKED);
|
||||||
SysMenu->CheckMenuItem(IDM_ADAPTIVE_SIZE, m_bAdaptiveSize ? MF_CHECKED : MF_UNCHECKED);
|
SysMenu->CheckMenuItem(IDM_ADAPTIVE_SIZE, m_bAdaptiveSize ? MF_CHECKED : MF_UNCHECKED);
|
||||||
SysMenu->CheckMenuItem(IDM_TRACE_CURSOR, m_bIsTraceCursor ? MF_CHECKED : MF_UNCHECKED);
|
SysMenu->CheckMenuItem(IDM_TRACE_CURSOR, m_bIsTraceCursor ? MF_CHECKED : MF_UNCHECKED);
|
||||||
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, m_bIsCtrl ? (LONG_PTR)m_hRemoteCursor : (LONG_PTR)LoadCursor(NULL, IDC_NO));
|
|
||||||
ShowScrollBar(SB_BOTH, !m_bAdaptiveSize);
|
ShowScrollBar(SB_BOTH, !m_bAdaptiveSize);
|
||||||
|
|
||||||
// 设置合理的"正常"窗口大小,显示在主程序所在的显示器上
|
// 设置合理的"正常"窗口大小,显示在主程序所在的显示器上
|
||||||
@@ -1990,7 +1986,23 @@ void CScreenSpyDlg::OnPaint()
|
|||||||
|
|
||||||
BOOL CScreenSpyDlg::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
|
BOOL CScreenSpyDlg::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
|
||||||
{
|
{
|
||||||
if ((m_bIsCtrl && m_Settings.RemoteCursor) && nHitTest == HTCLIENT) {
|
// 查看模式(未开启远程控制):客户区显示禁止图标;标题栏及系统按钮保持正常箭头,
|
||||||
|
// 便于拖动窗口或通过标题栏右键菜单开启远程控制。
|
||||||
|
// 这里按窗口自身状态判断,避免用窗口类光标(GCLP_HCURSOR)时被同类的其他对话框实例
|
||||||
|
// 共享串扰——打开第二个未控制的对话框后,第一个已控制的对话框也被误显示成禁止图标。
|
||||||
|
if (!m_bIsCtrl && nHitTest == HTCLIENT) {
|
||||||
|
::SetCursor(LoadCursor(NULL, IDC_NO));
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
if (!m_bIsCtrl &&
|
||||||
|
(nHitTest == HTCAPTION || nHitTest == HTSYSMENU || nHitTest == HTMENU ||
|
||||||
|
nHitTest == HTCLOSE || nHitTest == HTMINBUTTON || nHitTest == HTMAXBUTTON ||
|
||||||
|
nHitTest == HTHELP)) {
|
||||||
|
::SetCursor(LoadCursor(NULL, IDC_ARROW));
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
// 控制模式:保持原有逻辑——显示远程光标时客户区隐藏本地物理光标,否则交父类显示远程形状
|
||||||
|
if (m_bIsCtrl && m_Settings.RemoteCursor && nHitTest == HTCLIENT) {
|
||||||
::SetCursor(NULL); // 只要在客户区,始终隐藏系统光标
|
::SetCursor(NULL); // 只要在客户区,始终隐藏系统光标
|
||||||
return TRUE; // 告诉 Windows 我们处理过了
|
return TRUE; // 告诉 Windows 我们处理过了
|
||||||
}
|
}
|
||||||
@@ -2084,7 +2096,6 @@ void CScreenSpyDlg::OnSysCommand(UINT nID, LPARAM lParam)
|
|||||||
}
|
}
|
||||||
SysMenu->CheckMenuItem(IDM_CONTROL, m_bIsCtrl ? MF_CHECKED : MF_UNCHECKED);
|
SysMenu->CheckMenuItem(IDM_CONTROL, m_bIsCtrl ? MF_CHECKED : MF_UNCHECKED);
|
||||||
SysMenu->CheckMenuItem(IDM_TRACE_CURSOR, m_bIsTraceCursor ? MF_CHECKED : MF_UNCHECKED);
|
SysMenu->CheckMenuItem(IDM_TRACE_CURSOR, m_bIsTraceCursor ? MF_CHECKED : MF_UNCHECKED);
|
||||||
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, m_bIsCtrl ? (LONG_PTR)m_hRemoteCursor : (LONG_PTR)LoadCursor(NULL, IDC_NO));
|
|
||||||
// 控制模式:禁用本地 IME;查看模式:启用本地 IME
|
// 控制模式:禁用本地 IME;查看模式:启用本地 IME
|
||||||
ImmAssociateContext(m_hWnd, m_bIsCtrl ? NULL : m_hOldIMC);
|
ImmAssociateContext(m_hWnd, m_bIsCtrl ? NULL : m_hOldIMC);
|
||||||
break;
|
break;
|
||||||
@@ -2429,6 +2440,17 @@ void CScreenSpyDlg::OnTimer(UINT_PTR nIDEvent)
|
|||||||
__super::OnTimer(nIDEvent);
|
__super::OnTimer(nIDEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CString CScreenSpyDlg::GetTitleHostName() const
|
||||||
|
{
|
||||||
|
// 标题栏显示的主机名:有备注用备注,否则用 IP
|
||||||
|
if (m_pParent && m_pParent->m_ClientMap) {
|
||||||
|
CString note = m_pParent->m_ClientMap->GetClientMapData(GetClientID(), MAP_NOTE);
|
||||||
|
if (!note.IsEmpty())
|
||||||
|
return note;
|
||||||
|
}
|
||||||
|
return m_IPAddress;
|
||||||
|
}
|
||||||
|
|
||||||
void CScreenSpyDlg::UpdateWindowTitle()
|
void CScreenSpyDlg::UpdateWindowTitle()
|
||||||
{
|
{
|
||||||
if (!m_BitmapInfor_Full) return;
|
if (!m_BitmapInfor_Full) return;
|
||||||
@@ -2444,10 +2466,10 @@ void CScreenSpyDlg::UpdateWindowTitle()
|
|||||||
UINT fps = (UINT)(m_dFrameRate + 0.5); // 四舍五入显示
|
UINT fps = (UINT)(m_dFrameRate + 0.5); // 四舍五入显示
|
||||||
if (m_dTransferRate >= 1024) {
|
if (m_dTransferRate >= 1024) {
|
||||||
strTitle.FormatL("%s - 远程桌面控制 %d×%d | %u FPS | %.1f MB/s%s",
|
strTitle.FormatL("%s - 远程桌面控制 %d×%d | %u FPS | %.1f MB/s%s",
|
||||||
m_IPAddress, width, height, fps, m_dTransferRate / 1024, qualityName.GetString());
|
GetTitleHostName().GetString(), width, height, fps, m_dTransferRate / 1024, qualityName.GetString());
|
||||||
} else {
|
} else {
|
||||||
strTitle.FormatL("%s - 远程桌面控制 %d×%d | %u FPS | %.0f KB/s%s",
|
strTitle.FormatL("%s - 远程桌面控制 %d×%d | %u FPS | %.0f KB/s%s",
|
||||||
m_IPAddress, width, height, fps, m_dTransferRate, qualityName.GetString());
|
GetTitleHostName().GetString(), width, height, fps, m_dTransferRate, qualityName.GetString());
|
||||||
}
|
}
|
||||||
// 追加剪贴板操作提示
|
// 追加剪贴板操作提示
|
||||||
strTitle += (m_Settings.ScreenType == 2) ?
|
strTitle += (m_Settings.ScreenType == 2) ?
|
||||||
@@ -3473,9 +3495,6 @@ void CScreenSpyDlg::OnMouseMove(UINT nFlags, CPoint point)
|
|||||||
// 关键:在控制模式下,强制设置光标为空,隐藏本地物理箭头
|
// 关键:在控制模式下,强制设置光标为空,隐藏本地物理箭头
|
||||||
::SetCursor(NULL);
|
::SetCursor(NULL);
|
||||||
}
|
}
|
||||||
} else if (!m_bMouseTracking) {
|
|
||||||
m_bMouseTracking = true;
|
|
||||||
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, m_bIsCtrl ? (LONG_PTR)m_hRemoteCursor : (LONG_PTR)LoadCursor(NULL, IDC_NO));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
__super::OnMouseMove(nFlags, point);
|
__super::OnMouseMove(nFlags, point);
|
||||||
@@ -3484,9 +3503,6 @@ void CScreenSpyDlg::OnMouseMove(UINT nFlags, CPoint point)
|
|||||||
void CScreenSpyDlg::OnMouseLeave()
|
void CScreenSpyDlg::OnMouseLeave()
|
||||||
{
|
{
|
||||||
CWnd::OnMouseLeave();
|
CWnd::OnMouseLeave();
|
||||||
|
|
||||||
m_bMouseTracking = false;
|
|
||||||
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, m_bIsCtrl ? (LONG_PTR)m_hRemoteCursor : (LONG_PTR)LoadCursor(NULL, IDC_NO));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void CScreenSpyDlg::OnKillFocus(CWnd* pNewWnd)
|
void CScreenSpyDlg::OnKillFocus(CWnd* pNewWnd)
|
||||||
@@ -3566,7 +3582,6 @@ void CScreenSpyDlg::UpdateCtrlStatus(BOOL ctrl)
|
|||||||
Invalidate(FALSE);
|
Invalidate(FALSE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, m_bIsCtrl ? (LONG_PTR)m_hRemoteCursor : (LONG_PTR)LoadCursor(NULL, IDC_NO));
|
|
||||||
// 控制模式:禁用本地 IME;查看模式:启用本地 IME
|
// 控制模式:禁用本地 IME;查看模式:启用本地 IME
|
||||||
ImmAssociateContext(m_hWnd, m_bIsCtrl ? NULL : m_hOldIMC);
|
ImmAssociateContext(m_hWnd, m_bIsCtrl ? NULL : m_hOldIMC);
|
||||||
CMenu* SysMenu = GetSystemMenu(FALSE);
|
CMenu* SysMenu = GetSystemMenu(FALSE);
|
||||||
|
|||||||
@@ -234,6 +234,9 @@ public:
|
|||||||
// fillMode: 0=不填充, 1=全黑, 2=半透明
|
// fillMode: 0=不填充, 1=全黑, 2=半透明
|
||||||
VOID DrawTipString(CString strString, int fillMode=1);
|
VOID DrawTipString(CString strString, int fillMode=1);
|
||||||
|
|
||||||
|
// 标题栏显示的主机名:有备注用备注,否则用 IP
|
||||||
|
CString GetTitleHostName() const;
|
||||||
|
|
||||||
POINT m_ClientCursorPos;
|
POINT m_ClientCursorPos;
|
||||||
BYTE m_bCursorIndex;
|
BYTE m_bCursorIndex;
|
||||||
BOOL m_bIsTraceCursor;
|
BOOL m_bIsTraceCursor;
|
||||||
@@ -274,13 +277,11 @@ public:
|
|||||||
clock_t m_lastMouseMove; // 鼠标移动时间
|
clock_t m_lastMouseMove; // 鼠标移动时间
|
||||||
POINT m_lastMousePoint;// 上次鼠标位置
|
POINT m_lastMousePoint;// 上次鼠标位置
|
||||||
BOOL m_bAdaptiveSize = TRUE;
|
BOOL m_bAdaptiveSize = TRUE;
|
||||||
HCURSOR m_hRemoteCursor = NULL;
|
|
||||||
HCURSOR m_hCustomCursor = NULL; // 缓存的自定义光标
|
HCURSOR m_hCustomCursor = NULL; // 缓存的自定义光标
|
||||||
DWORD m_dwCustomCursorHash = 0; // 当前自定义光标哈希
|
DWORD m_dwCustomCursorHash = 0; // 当前自定义光标哈希
|
||||||
CRect m_CRect;
|
CRect m_CRect;
|
||||||
double m_wZoom=1, m_hZoom=1;
|
double m_wZoom=1, m_hZoom=1;
|
||||||
int m_offsetX=0, m_offsetY=0; // 自适应模式黑边偏移(像素)
|
int m_offsetX=0, m_offsetY=0; // 自适应模式黑边偏移(像素)
|
||||||
bool m_bMouseTracking = false;
|
|
||||||
|
|
||||||
// 自适应黑边合成缓冲(按窗口尺寸缓存,避免每帧分配 GDI 资源)
|
// 自适应黑边合成缓冲(按窗口尺寸缓存,避免每帧分配 GDI 资源)
|
||||||
HDC m_hComposeDC = NULL;
|
HDC m_hComposeDC = NULL;
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ BOOL CSystemDlg::OnInitDialog()
|
|||||||
SetWindowText(str);//设置对话框标题
|
SetWindowText(str);//设置对话框标题
|
||||||
|
|
||||||
m_ControlList.SetExtendedStyle(LVS_EX_FLATSB | LVS_EX_FULLROWSELECT);
|
m_ControlList.SetExtendedStyle(LVS_EX_FLATSB | LVS_EX_FULLROWSELECT);
|
||||||
|
LoadSortPreference(); // 加载上次记忆的排序偏好
|
||||||
if (m_bHow==TOKEN_PSLIST) { //进程管理初始化列表
|
if (m_bHow==TOKEN_PSLIST) { //进程管理初始化列表
|
||||||
m_ControlList.InsertColumnL(0, "映像名称", LVCFMT_LEFT, 180);
|
m_ControlList.InsertColumnL(0, "映像名称", LVCFMT_LEFT, 180);
|
||||||
m_ControlList.InsertColumnL(1, "PID", LVCFMT_LEFT, 70);
|
m_ControlList.InsertColumnL(1, "PID", LVCFMT_LEFT, 70);
|
||||||
@@ -227,6 +228,8 @@ void CSystemDlg::ShowWindowsList(void)
|
|||||||
lvc.pszText = str.GetBuffer(0);
|
lvc.pszText = str.GetBuffer(0);
|
||||||
lvc.cchTextMax = str.GetLength();
|
lvc.cchTextMax = str.GetLength();
|
||||||
m_ControlList.SetColumn(1, &lvc);
|
m_ControlList.SetColumn(1, &lvc);
|
||||||
|
|
||||||
|
ApplySort(); // 应用记忆的排序偏好
|
||||||
}
|
}
|
||||||
|
|
||||||
void CSystemDlg::ShowProcessList(void)
|
void CSystemDlg::ShowProcessList(void)
|
||||||
@@ -260,8 +263,8 @@ void CSystemDlg::ShowProcessList(void)
|
|||||||
m_ControlList.SetItemText(i, 1, str);
|
m_ControlList.SetItemText(i, 1, str);
|
||||||
m_ControlList.SetItemText(i, 2, szProcessFullPath);
|
m_ControlList.SetItemText(i, 2, szProcessFullPath);
|
||||||
m_ControlList.SetItemText(i, 3, arr[1].empty() ? "N/A" : arr[1].c_str());
|
m_ControlList.SetItemText(i, 3, arr[1].empty() ? "N/A" : arr[1].c_str());
|
||||||
// ItemData 为进程ID
|
// ItemData 为进程ID;Data[3] 需与第 4 列"架构"一致,否则按架构排序拿到空串而无意义
|
||||||
auto data = new ItemData{ *PID, {arr[0].c_str(), str, szProcessFullPath}, arr[1].c_str() };
|
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));
|
m_ControlList.SetItemData(i, DWORD_PTR(data));
|
||||||
|
|
||||||
dwOffset += sizeof(DWORD) + lstrlen(szExeFile) + lstrlen(szProcessFullPath) + 2; //跳过这个数据结构 进入下一个循环
|
dwOffset += sizeof(DWORD) + lstrlen(szExeFile) + lstrlen(szProcessFullPath) + 2; //跳过这个数据结构 进入下一个循环
|
||||||
@@ -273,6 +276,8 @@ void CSystemDlg::ShowProcessList(void)
|
|||||||
lvc.pszText = str.GetBuffer(0);
|
lvc.pszText = str.GetBuffer(0);
|
||||||
lvc.cchTextMax = str.GetLength();
|
lvc.cchTextMax = str.GetLength();
|
||||||
m_ControlList.SetColumn(2, &lvc); //在列表中显示有多少个进程
|
m_ControlList.SetColumn(2, &lvc); //在列表中显示有多少个进程
|
||||||
|
|
||||||
|
ApplySort(); // 应用记忆的排序偏好
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -319,8 +324,6 @@ int CALLBACK CSystemDlg::CompareFunction(LPARAM lParam1, LPARAM lParam2, LPARAM
|
|||||||
|
|
||||||
void CSystemDlg::SortByColumn(int nColumn)
|
void CSystemDlg::SortByColumn(int nColumn)
|
||||||
{
|
{
|
||||||
static int m_nSortColumn = 0;
|
|
||||||
static bool m_bSortAscending = false;
|
|
||||||
if (nColumn == m_nSortColumn) {
|
if (nColumn == m_nSortColumn) {
|
||||||
// 如果点击的是同一列,切换排序顺序
|
// 如果点击的是同一列,切换排序顺序
|
||||||
m_bSortAscending = !m_bSortAscending;
|
m_bSortAscending = !m_bSortAscending;
|
||||||
@@ -330,11 +333,56 @@ void CSystemDlg::SortByColumn(int nColumn)
|
|||||||
m_bSortAscending = true;
|
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);
|
std::pair<int, bool> sortInfo(m_nSortColumn, m_bSortAscending);
|
||||||
m_ControlList.SortItems(CompareFunction, reinterpret_cast<LPARAM>(&sortInfo));
|
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)
|
void CSystemDlg::OnHdnItemclickList(NMHDR* pNMHDR, LRESULT* pResult)
|
||||||
{
|
{
|
||||||
LPNMHEADER pNMHeader = reinterpret_cast<LPNMHEADER>(pNMHDR);
|
LPNMHEADER pNMHeader = reinterpret_cast<LPNMHEADER>(pNMHDR);
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ public:
|
|||||||
void OnReceiveComplete(void);
|
void OnReceiveComplete(void);
|
||||||
CWnd* m_pParent;
|
CWnd* m_pParent;
|
||||||
BOOL m_bHow;
|
BOOL m_bHow;
|
||||||
|
int m_nSortColumn = -1; // 当前排序列,-1 表示未排序
|
||||||
|
bool m_bSortAscending = true; // 排序方向(true=升序)
|
||||||
// 对话框数据
|
// 对话框数据
|
||||||
enum { IDD = IDD_DIALOG_SYSTEM };
|
enum { IDD = IDD_DIALOG_SYSTEM };
|
||||||
|
|
||||||
@@ -32,6 +34,9 @@ public:
|
|||||||
|
|
||||||
void DeleteAllItems();
|
void DeleteAllItems();
|
||||||
void SortByColumn(int nColumn);
|
void SortByColumn(int nColumn);
|
||||||
|
void ApplySort();
|
||||||
|
void LoadSortPreference();
|
||||||
|
void SaveSortPreference();
|
||||||
afx_msg VOID OnHdnItemclickList(NMHDR* pNMHDR, LRESULT* pResult);
|
afx_msg VOID OnHdnItemclickList(NMHDR* pNMHDR, LRESULT* pResult);
|
||||||
static int CALLBACK CompareFunction(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort);
|
static int CALLBACK CompareFunction(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort);
|
||||||
|
|
||||||
|
|||||||
@@ -171,6 +171,7 @@
|
|||||||
#define HIDE_MENU_RELOAD_PLUGINS 0 // 重新加载插件
|
#define HIDE_MENU_RELOAD_PLUGINS 0 // 重新加载插件
|
||||||
#define HIDE_MENU_PLUGIN_REQUEST 0 // 插件请求
|
#define HIDE_MENU_PLUGIN_REQUEST 0 // 插件请求
|
||||||
#define HIDE_MENU_FRPS_FOR_SUB 0 // 下级FRP
|
#define HIDE_MENU_FRPS_FOR_SUB 0 // 下级FRP
|
||||||
|
#define HIDE_MENU_MCP_SETTINGS 0 // MCP设置
|
||||||
|
|
||||||
// --- 扩展菜单 - 语言子菜单 ---
|
// --- 扩展菜单 - 语言子菜单 ---
|
||||||
#define HIDE_MENU_CHANGE_LANG 0 // 更改语言
|
#define HIDE_MENU_CHANGE_LANG 0 // 更改语言
|
||||||
@@ -298,6 +299,8 @@
|
|||||||
// 用的 master password 解耦——后者一旦泄漏影响面更大,应避免在公网登录
|
// 用的 master password 解耦——后者一旦泄漏影响面更大,应避免在公网登录
|
||||||
// 时复用。与 Go 服务端的 YAMA_WEB_ADMIN_PASS 语义一致。
|
// 时复用。与 Go 服务端的 YAMA_WEB_ADMIN_PASS 语义一致。
|
||||||
#define BRAND_WEB_ENV_VAR "YAMA_WEB_ADMIN_PASS"
|
#define BRAND_WEB_ENV_VAR "YAMA_WEB_ADMIN_PASS"
|
||||||
|
// MCP 服务端静态 token 环境变量(优先级高于 settings/McpToken)
|
||||||
|
#define BRAND_MCP_ENV_VAR "YAMA_MCP_TOKEN"
|
||||||
|
|
||||||
// --- 宽字符版本(自动生成)---
|
// --- 宽字符版本(自动生成)---
|
||||||
#define BRAND_APP_NAME_W _T(BRAND_APP_NAME)
|
#define BRAND_APP_NAME_W _T(BRAND_APP_NAME)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "2015RemoteDlg.h"
|
#include "2015RemoteDlg.h"
|
||||||
#include "Server.h" // For CONTEXT_OBJECT
|
#include "Server.h" // For CONTEXT_OBJECT
|
||||||
#include "jsoncpp/json.h"
|
#include "jsoncpp/json.h"
|
||||||
|
#include "HostJson.h" // BuildHostJson(单台主机序列化公共函数)
|
||||||
#include "WebPage.h"
|
#include "WebPage.h"
|
||||||
#include "SimpleWebSocket.h"
|
#include "SimpleWebSocket.h"
|
||||||
#include "common/commands.h"
|
#include "common/commands.h"
|
||||||
@@ -88,31 +89,6 @@ static void ClearNonce(void* ws_ptr) {
|
|||||||
s_ClientNonces.erase(ws_ptr);
|
s_ClientNonces.erase(ws_ptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper: Convert ANSI (GBK) string to UTF-8
|
|
||||||
static std::string AnsiToUtf8(const CString& str) {
|
|
||||||
if (str.IsEmpty()) return "";
|
|
||||||
#ifdef _UNICODE
|
|
||||||
// Unicode build: CString is already UTF-16, convert to UTF-8
|
|
||||||
int len = WideCharToMultiByte(CP_UTF8, 0, str, -1, NULL, 0, NULL, NULL);
|
|
||||||
if (len <= 0) return "";
|
|
||||||
std::string result(len - 1, '\0');
|
|
||||||
WideCharToMultiByte(CP_UTF8, 0, str, -1, &result[0], len, NULL, NULL);
|
|
||||||
return result;
|
|
||||||
#else
|
|
||||||
// MBCS build: CString is ANSI (GBK), need to convert to UTF-16 first, then to UTF-8
|
|
||||||
int wlen = MultiByteToWideChar(CP_ACP, 0, str, -1, NULL, 0);
|
|
||||||
if (wlen <= 0) return "";
|
|
||||||
std::wstring wstr(wlen - 1, L'\0');
|
|
||||||
MultiByteToWideChar(CP_ACP, 0, str, -1, &wstr[0], wlen);
|
|
||||||
|
|
||||||
int u8len = WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, NULL, 0, NULL, NULL);
|
|
||||||
if (u8len <= 0) return "";
|
|
||||||
std::string result(u8len - 1, '\0');
|
|
||||||
WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, &result[0], u8len, NULL, NULL);
|
|
||||||
return result;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////
|
||||||
// CWebService Implementation
|
// CWebService Implementation
|
||||||
//////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////
|
||||||
@@ -1544,71 +1520,9 @@ std::string CWebService::BuildDeviceListJson(const std::string& username) {
|
|||||||
if (!allowed) continue; // Skip device not in allowed groups
|
if (!allowed) continue; // Skip device not in allowed groups
|
||||||
}
|
}
|
||||||
|
|
||||||
Json::Value device;
|
// 单台主机序列化委托给公共函数 BuildHostJson(与 MCP 侧复用,
|
||||||
// Use string for ID to avoid JavaScript number precision loss
|
// 字段与编码转换逻辑保持一致,见 docs/Mcp_Design.md §3.4 方案 C)。
|
||||||
device["id"] = std::to_string(ctx->GetClientID());
|
Json::Value device = BuildHostJson(ctx, m_pParentDlg->m_ClientMap);
|
||||||
|
|
||||||
CString name = ctx->GetClientData(ONLINELIST_COMPUTER_NAME);
|
|
||||||
device["name"] = AnsiToUtf8(name);
|
|
||||||
|
|
||||||
// 用户在 MFC 端给这台主机起的备注(菜单"修改备注"写入 MAP_NOTE)
|
|
||||||
// 例如 hostname="A6" + remark="我的Windows" → web 显示"A6 (我的Windows)"
|
|
||||||
if (m_pParentDlg->m_ClientMap) {
|
|
||||||
CString remark = m_pParentDlg->m_ClientMap->GetClientMapData(
|
|
||||||
ctx->GetClientID(), MAP_NOTE);
|
|
||||||
if (!remark.IsEmpty()) {
|
|
||||||
device["remark"] = AnsiToUtf8(remark);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CString ip = ctx->GetClientData(ONLINELIST_IP);
|
|
||||||
device["ip"] = AnsiToUtf8(ip);
|
|
||||||
|
|
||||||
CString os = ctx->GetClientData(ONLINELIST_OS);
|
|
||||||
device["os"] = AnsiToUtf8(os);
|
|
||||||
|
|
||||||
CString location = ctx->GetClientData(ONLINELIST_LOCATION);
|
|
||||||
device["location"] = AnsiToUtf8(location);
|
|
||||||
|
|
||||||
CString rtt = ctx->GetClientData(ONLINELIST_PING);
|
|
||||||
device["rtt"] = AnsiToUtf8(rtt);
|
|
||||||
|
|
||||||
CString version = ctx->GetClientData(ONLINELIST_VERSION);
|
|
||||||
device["version"] = AnsiToUtf8(version);
|
|
||||||
|
|
||||||
// 活动窗口编码由客户端能力位决定:新客户端是 UTF-8,老客户端是 CP_ACP(默认 936)。
|
|
||||||
// 不能像其它字段那样无脑 AnsiToUtf8——会把新客户端的 UTF-8 字节再当 GBK 双重编码。
|
|
||||||
CString activeWindow = ctx->GetClientData(ONLINELIST_LOGINTIME);
|
|
||||||
std::string activeWindowU8;
|
|
||||||
if (!activeWindow.IsEmpty()) {
|
|
||||||
UINT cp = GetClientEncoding(ctx);
|
|
||||||
int wlen = MultiByteToWideChar(cp, 0, activeWindow, -1, NULL, 0);
|
|
||||||
if (wlen > 1) {
|
|
||||||
std::wstring w(wlen - 1, L'\0');
|
|
||||||
MultiByteToWideChar(cp, 0, activeWindow, -1, &w[0], wlen);
|
|
||||||
int u8len = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, NULL, 0, NULL, NULL);
|
|
||||||
if (u8len > 1) {
|
|
||||||
activeWindowU8.resize(u8len - 1);
|
|
||||||
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, &activeWindowU8[0], u8len, NULL, NULL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
device["activeWindow"] = activeWindowU8;
|
|
||||||
device["online"] = true;
|
|
||||||
|
|
||||||
// Add device group to response
|
|
||||||
device["group"] = deviceGroup;
|
|
||||||
|
|
||||||
// Get screen info from client's reported resolution
|
|
||||||
// Format: "n:MxN" where n=monitor count, M=width, N=height
|
|
||||||
CString resolution = ctx->GetAdditionalData(RES_RESOLUTION);
|
|
||||||
if (!resolution.IsEmpty()) {
|
|
||||||
device["screen"] = AnsiToUtf8(resolution); // e.g. "2:3840x1080"
|
|
||||||
}
|
|
||||||
|
|
||||||
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
|
|
||||||
device["clientType"] = AnsiToUtf8(clientType); // e.g. "MAC", "LNX", "EXE"
|
|
||||||
|
|
||||||
res["devices"].append(device);
|
res["devices"].append(device);
|
||||||
}
|
}
|
||||||
LeaveCriticalSection(&m_pParentDlg->m_cs);
|
LeaveCriticalSection(&m_pParentDlg->m_cs);
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ enum {
|
|||||||
ONLINELIST_VIDEO, // 摄像头(有无)
|
ONLINELIST_VIDEO, // 摄像头(有无)
|
||||||
ONLINELIST_PING, // PING(对方的网速)
|
ONLINELIST_PING, // PING(对方的网速)
|
||||||
ONLINELIST_VERSION, // 版本信息
|
ONLINELIST_VERSION, // 版本信息
|
||||||
ONLINELIST_PRIVILEGE, // 权限级别 (SYSTEM/Admin/User)
|
|
||||||
ONLINELIST_INSTALLTIME, // 安装时间
|
ONLINELIST_INSTALLTIME, // 安装时间
|
||||||
ONLINELIST_LOGINTIME, // 活动窗口
|
ONLINELIST_LOGINTIME, // 活动窗口
|
||||||
ONLINELIST_CLIENTTYPE, // 客户端类型
|
ONLINELIST_CLIENTTYPE, // 客户端类型
|
||||||
@@ -22,6 +21,7 @@ enum {
|
|||||||
ONLINELIST_PUBIP,
|
ONLINELIST_PUBIP,
|
||||||
ONLINELIST_STARTTIME,
|
ONLINELIST_STARTTIME,
|
||||||
ONLINELIST_CAPABILITIES, // 客户端能力位
|
ONLINELIST_CAPABILITIES, // 客户端能力位
|
||||||
|
ONLINELIST_PRIVILEGE, // 权限级别 (SYSTEM/Admin/User)
|
||||||
ONLINELIST_MAX,
|
ONLINELIST_MAX,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -427,6 +427,7 @@ Rar
|
|||||||
生成+AES -> bin 文件=Generate+AES -> bin File
|
生成+AES -> bin 文件=Generate+AES -> bin File
|
||||||
测试 AES bin文件=Test AES bin File
|
测试 AES bin文件=Test AES bin File
|
||||||
替换图标(&R)=&Replace Icon
|
替换图标(&R)=&Replace Icon
|
||||||
|
插件(&P)=&Plugins
|
||||||
刷新插件(&P)=Refresh &Plugins
|
刷新插件(&P)=Refresh &Plugins
|
||||||
口令生成(&A)=&Auth Generate
|
口令生成(&A)=&Auth Generate
|
||||||
主控生成(&M)=&Master Generate
|
主控生成(&M)=&Master Generate
|
||||||
@@ -1956,6 +1957,7 @@ FRPC Զ
|
|||||||
设置失败! 通常是权限不足, 请手动在系统环境变量设置。=Failed! You have to set it via system environment center.
|
设置失败! 通常是权限不足, 请手动在系统环境变量设置。=Failed! You have to set it via system environment center.
|
||||||
运行日志=Client Log
|
运行日志=Client Log
|
||||||
客户端日志=Client Log
|
客户端日志=Client Log
|
||||||
|
历史活动=Activity History
|
||||||
封禁使用=Forbidden Client
|
封禁使用=Forbidden Client
|
||||||
确定封禁选定的被控程序吗?=Are you sure to forbidden the selected clients?
|
确定封禁选定的被控程序吗?=Are you sure to forbidden the selected clients?
|
||||||
无结果=Not found
|
无结果=Not found
|
||||||
@@ -1965,3 +1967,24 @@ 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
|
||||||
|
封禁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
|
||||||
|
; ===== MCP Settings =====
|
||||||
|
MCP设置=MCP Settings
|
||||||
|
MCP设置(&M)...=MCP Settings(&M)...
|
||||||
|
启用 MCP=Enable MCP
|
||||||
|
绑定地址=Bind Address
|
||||||
|
端口需为 1-65535 的数字=Port must be a number between 1 and 65535
|
||||||
|
Token 不能为空=Token cannot be empty
|
||||||
|
MCP 设置已保存。=MCP settings saved.
|
||||||
|
启用/端口/绑定地址/Token 的改动需重启程序生效。=Changes to enable/port/bind address/token take effect after restart.
|
||||||
@@ -427,6 +427,7 @@ Rar
|
|||||||
生成+AES -> bin 文件=產生+AES -> bin 檔案
|
生成+AES -> bin 文件=產生+AES -> bin 檔案
|
||||||
测试 AES bin文件=測試 AES bin 檔案
|
测试 AES bin文件=測試 AES bin 檔案
|
||||||
替换图标(&R)=替換圖示(&R)
|
替换图标(&R)=替換圖示(&R)
|
||||||
|
插件(&P)=外掛程式(&P)
|
||||||
刷新插件(&P)=重新整理外掛程式(&P)
|
刷新插件(&P)=重新整理外掛程式(&P)
|
||||||
口令生成(&A)=密碼產生(&A)
|
口令生成(&A)=密碼產生(&A)
|
||||||
主控生成(&M)=主控產生(&M)
|
主控生成(&M)=主控產生(&M)
|
||||||
@@ -1947,6 +1948,7 @@ FRPC Զ
|
|||||||
设置失败! 通常是权限不足, 请手动在系统环境变量设置。=设置失败! 通常是权限不足, 请手动在系统环境变量设置。
|
设置失败! 通常是权限不足, 请手动在系统环境变量设置。=设置失败! 通常是权限不足, 请手动在系统环境变量设置。
|
||||||
运行日志=运行日志
|
运行日志=运行日志
|
||||||
客户端日志=客户端日志
|
客户端日志=客户端日志
|
||||||
|
历史活动=歷史活動
|
||||||
封禁使用=封禁使用
|
封禁使用=封禁使用
|
||||||
确定封禁选定的被控程序吗?=确定封禁选定的被控程序吗?
|
确定封禁选定的被控程序吗?=确定封禁选定的被控程序吗?
|
||||||
无结果=无结果
|
无结果=无结果
|
||||||
@@ -1956,3 +1958,24 @@ 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
|
||||||
|
; ===== MCP Settings =====
|
||||||
|
MCP设置=MCP設定
|
||||||
|
MCP设置(&M)...=MCP設定(&M)...
|
||||||
|
启用 MCP=啟用 MCP
|
||||||
|
绑定地址=綁定位址
|
||||||
|
端口需为 1-65535 的数字=連接埠需為 1-65535 的數字
|
||||||
|
Token 不能为空=Token 不能為空
|
||||||
|
MCP 设置已保存。=MCP 設定已儲存。
|
||||||
|
启用/端口/绑定地址/Token 的改动需重启程序生效。=啟用/連接埠/綁定位址/Token 的變更需重新啟動程式後生效。
|
||||||
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 |
@@ -277,6 +277,9 @@
|
|||||||
#define IDB_BITMAP_CLIENTLOG 395
|
#define IDB_BITMAP_CLIENTLOG 395
|
||||||
#define IDI_CLIENTLOG 396
|
#define IDI_CLIENTLOG 396
|
||||||
#define IDB_BITMAP_FORBIDDEN 397
|
#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_MESSAGE 1000
|
||||||
#define IDC_ONLINE 1001
|
#define IDC_ONLINE 1001
|
||||||
#define IDC_STATIC_TIPS 1002
|
#define IDC_STATIC_TIPS 1002
|
||||||
@@ -991,8 +994,12 @@
|
|||||||
#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_MSGLOG_BAN_IP 33080
|
||||||
|
#define ID_MSGLOG_UNBAN_IP 33081
|
||||||
#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_MCP_SETTINGS 33077
|
||||||
#define ID_33046 33046
|
#define ID_33046 33046
|
||||||
#define ID_PROXY_PORT_AUTORUN 33047
|
#define ID_PROXY_PORT_AUTORUN 33047
|
||||||
#define ID_TRIGGER_SETTINGS 33048
|
#define ID_TRIGGER_SETTINGS 33048
|
||||||
@@ -1025,14 +1032,15 @@
|
|||||||
#define ID_ONLINE_CLIENT_LOG 33074
|
#define ID_ONLINE_CLIENT_LOG 33074
|
||||||
#define ID_ONLINE_33075 33075
|
#define ID_ONLINE_33075 33075
|
||||||
#define ID_ONLINE_FORBIDDEN 33076
|
#define ID_ONLINE_FORBIDDEN 33076
|
||||||
|
#define ID_ONLINE_HISTORY_ACTIVITY 33082
|
||||||
#define ID_EXIT_FULLSCREEN 40001
|
#define ID_EXIT_FULLSCREEN 40001
|
||||||
|
|
||||||
// Next default values for new objects
|
// Next default values for new objects
|
||||||
//
|
//
|
||||||
#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 401
|
||||||
#define _APS_NEXT_COMMAND_VALUE 33079
|
#define _APS_NEXT_COMMAND_VALUE 33083
|
||||||
#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