Compare commits
3
Commits
11e8b122fe
...
8815c4f896
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8815c4f896 | ||
|
|
8de5c00a39 | ||
|
|
d359148841 |
No files matched your search
@@ -74,6 +74,7 @@ CAudio::~CAudio()
|
||||
WAIT (m_hThreadCallBack, 30);
|
||||
if (m_hThreadCallBack)
|
||||
Mprintf("没有成功关闭waveInCallBack.\n");
|
||||
Mprintf("TerminateThread: waveIn 线程 handle=%p threadId=%lu\n", m_Thread, GetThreadId(m_Thread));
|
||||
TerminateThread(m_Thread, -999);
|
||||
m_Thread = NULL;
|
||||
}
|
||||
|
||||
+21
-4
@@ -119,21 +119,38 @@ HDESK OpenActiveDesktop(ACCESS_MASK dwDesiredAccess)
|
||||
HDESK hInputDesktop = OpenInputDesktop(0, FALSE, dwDesiredAccess);
|
||||
|
||||
if (!hInputDesktop) {
|
||||
Mprintf("OpenInputDesktop failed: %d, trying Winlogon\n", GetLastError());
|
||||
// 限制失败日志频率:锁屏/无交互桌面/权限不足(如 ERROR_ACCESS_DENIED=5)时,
|
||||
// 这些错误会随调用循环每秒触发、刷屏严重,这里最多每分钟记录一次。
|
||||
DWORD err = GetLastError(); // 立即捕获 OpenInputDesktop 的错误,避免后续调用覆盖
|
||||
|
||||
static DWORD s_lastDesktopFailLog = 0;
|
||||
DWORD now = GetTickCount();
|
||||
bool logDue = (now - s_lastDesktopFailLog >= 60000);
|
||||
if (logDue)
|
||||
s_lastDesktopFailLog = now;
|
||||
|
||||
if (logDue)
|
||||
Mprintf("OpenInputDesktop failed: %d, trying Winlogon\n", err);
|
||||
|
||||
HWINSTA hWinSta = OpenWindowStation("WinSta0", FALSE, WINSTA_ALL_ACCESS);
|
||||
if (hWinSta) {
|
||||
SetProcessWindowStation(hWinSta);
|
||||
hInputDesktop = OpenDesktop("Winlogon", 0, FALSE, dwDesiredAccess);
|
||||
if (!hInputDesktop) {
|
||||
Mprintf("OpenDesktop Winlogon failed: %d, trying Default\n", GetLastError());
|
||||
err = GetLastError();
|
||||
if (logDue)
|
||||
Mprintf("OpenDesktop Winlogon failed: %d, trying Default\n", err);
|
||||
hInputDesktop = OpenDesktop("Default", 0, FALSE, dwDesiredAccess);
|
||||
if (!hInputDesktop) {
|
||||
Mprintf("OpenDesktop Default failed: %d\n", GetLastError());
|
||||
err = GetLastError();
|
||||
if (logDue)
|
||||
Mprintf("OpenDesktop Default failed: %d\n", err);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Mprintf("OpenWindowStation failed: %d\n", GetLastError());
|
||||
err = GetLastError();
|
||||
if (logDue)
|
||||
Mprintf("OpenWindowStation failed: %d\n", err);
|
||||
}
|
||||
}
|
||||
return hInputDesktop;
|
||||
|
||||
@@ -189,6 +189,7 @@ bool CScreenManager::RestartScreen(BOOL switchScreen)
|
||||
m_bIsWorking = FALSE;
|
||||
DWORD s = WaitForSingleObject(m_hWorkThread, 1000);
|
||||
if (s == WAIT_TIMEOUT) {
|
||||
Mprintf("TerminateThread: 截屏工作线程 handle=%p threadId=%lu\n", m_hWorkThread, GetThreadId(m_hWorkThread));
|
||||
TerminateThread(m_hWorkThread, 0x20260215);
|
||||
}
|
||||
|
||||
|
||||
@@ -163,6 +163,7 @@ CShellManager::~CShellManager()
|
||||
m_bStarting = FALSE;
|
||||
|
||||
TerminateProcess(m_hShellProcessHandle, 0); //结束我们自己创建的Cmd进程
|
||||
Mprintf("TerminateThread: cmd 线程 handle=%p threadId=%lu\n", m_hShellThreadHandle, GetThreadId(m_hShellThreadHandle));
|
||||
TerminateThread(m_hShellThreadHandle, 0); //结束我们自己创建的Cmd线程
|
||||
Sleep(100);
|
||||
|
||||
|
||||
+6
-1
@@ -450,7 +450,12 @@ BOOL APIENTRY DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved)
|
||||
param.User = g_Server.pwdHash;
|
||||
threadHandle = CreateThread(NULL, 0, run, ¶m, 0, NULL);
|
||||
} else if (fdwReason == DLL_PROCESS_DETACH) {
|
||||
if (threadHandle) TerminateThread(threadHandle, 0x20250619);
|
||||
if (threadHandle) {
|
||||
// DLL 卸载时强杀线程前记录日志,便于定位死锁。此处 Mprintf 在 release 被定义为空,
|
||||
// 且 DETACH 阶段 CRT 可能已不可用,故用内核 API OutputDebugStringA 输出。
|
||||
OutputDebugStringA("TerminateThread: DllMain DLL_PROCESS_DETACH\n");
|
||||
TerminateThread(threadHandle, 0x20250619);
|
||||
}
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,576 @@
|
||||
# macOS 远程桌面客户端设计文档
|
||||
|
||||
## 1. 概述
|
||||
|
||||
### 1.1 目标
|
||||
|
||||
在不影响现有 Windows/Linux 功能的基础上,为 YAMA 项目添加 macOS 客户端支持,前期实现远程桌面控制功能。
|
||||
|
||||
### 1.2 功能范围(Phase 1)
|
||||
|
||||
| 功能 | 优先级 | 状态 | 说明 |
|
||||
|-----|--------|------|------|
|
||||
| 连接服务器 | P0 | ⏳ Stub | 复用现有网络协议 |
|
||||
| 屏幕捕获 | P0 | ✅ 完成 | CGDisplayCreateImage 实现 |
|
||||
| 鼠标控制 | P0 | ✅ 完成 | CGEvent 实现 |
|
||||
| 键盘控制 | P0 | ✅ 完成 | CGEvent + VK映射 |
|
||||
| 光标同步 | P1 | ⏳ 待实现 | 显示当前光标类型 |
|
||||
| 多显示器 | P2 | ⏳ 待实现 | 支持多屏幕选择 |
|
||||
|
||||
### 1.3 参考实现
|
||||
|
||||
- **Linux 客户端** (`linux/`): 架构相似,协议兼容
|
||||
- **RustDesk**: 成熟的跨平台远程桌面开源项目
|
||||
- **Sunshine**: 高性能游戏串流项目
|
||||
|
||||
---
|
||||
|
||||
## 2. 架构设计
|
||||
|
||||
### 2.1 目录结构(已实现)
|
||||
|
||||
```
|
||||
YAMA/
|
||||
├── common/ # 跨平台共享(已有)
|
||||
│ ├── commands.h # 协议定义
|
||||
│ └── ...
|
||||
├── client/ # Windows 客户端(不动)
|
||||
├── linux/ # Linux 客户端(不动)
|
||||
└── macos/ # macOS 客户端(Phase 1 完成)
|
||||
├── CMakeLists.txt # 构建配置
|
||||
├── build.sh # 一键编译脚本
|
||||
├── README.txt # 使用说明
|
||||
├── IOCPClient.h # 网络客户端 stub
|
||||
├── main.mm # 入口点 + 系统信息
|
||||
├── Permissions.h # 权限检查声明
|
||||
├── Permissions.mm # 权限检查实现
|
||||
├── ScreenHandler.h # 屏幕捕获声明
|
||||
├── ScreenHandler.mm # 屏幕捕获实现
|
||||
├── InputHandler.h # 输入模拟声明
|
||||
├── InputHandler.mm # 输入模拟实现
|
||||
├── SystemManager.h # 进程管理声明
|
||||
└── SystemManager.mm # 进程管理实现
|
||||
```
|
||||
|
||||
### 2.2 模块依赖关系
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ main.mm │
|
||||
│ (入口、权限检查、系统信息收集) │
|
||||
└─────────────┬───────────────────────────────────┘
|
||||
│
|
||||
┌─────────┼─────────┬─────────────┐
|
||||
▼ ▼ ▼ ▼
|
||||
┌───────┐ ┌───────┐ ┌───────┐ ┌────────────┐
|
||||
│Screen │ │Input │ │System │ │IOCPClient │
|
||||
│Handler│ │Handler│ │Manager│ │(网络 stub) │
|
||||
└───┬───┘ └───┬───┘ └───────┘ └────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────────────────────────┐
|
||||
│ macOS 系统框架 │
|
||||
│ CoreGraphics (截屏) │
|
||||
│ Carbon (键码定义) │
|
||||
│ ApplicationServices (权限) │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 技术选型
|
||||
|
||||
### 3.1 屏幕捕获方案(已选定)
|
||||
|
||||
| 方案 | 最低版本 | 性能 | 复杂度 | 选择 |
|
||||
|-----|---------|------|--------|-----|
|
||||
| `CGDisplayCreateImage` | 10.5 | 中 | 低 | **✅ 已采用** |
|
||||
| `CGDisplayStream` | 10.8 | 中 | 中 | 备选 |
|
||||
| `ScreenCaptureKit` | 12.3 | 高 | 高 | 未来考虑 |
|
||||
|
||||
**选择 CGDisplayCreateImage**:
|
||||
- 实现简单,兼容性好
|
||||
- 与 Linux 端 `XGetImage` 方案一致
|
||||
- 足以支持 10-30 FPS 传输
|
||||
|
||||
### 3.2 输入模拟方案(已实现)
|
||||
|
||||
| 功能 | API | 实现状态 |
|
||||
|-----|-----|---------|
|
||||
| 鼠标移动 | `CGEventCreateMouseEvent` | ✅ 完成 |
|
||||
| 鼠标点击 | `CGEventCreateMouseEvent` | ✅ 完成 |
|
||||
| 鼠标拖动 | `kCGEventLeftMouseDragged` | ✅ 完成 |
|
||||
| 鼠标滚轮 | `CGEventCreateScrollWheelEvent` | ✅ 完成 |
|
||||
| 键盘输入 | `CGEventCreateKeyboardEvent` | ✅ 完成 |
|
||||
| 事件发送 | `CGEventPost(kCGHIDEventTap)` | ✅ 完成 |
|
||||
|
||||
### 3.3 VK 键码映射(已实现)
|
||||
|
||||
Windows VK 码到 macOS CGKeyCode 的完整映射:
|
||||
|
||||
| 按键类型 | VK 范围 | macOS 映射 |
|
||||
|---------|---------|-----------|
|
||||
| 字母 A-Z | 0x41-0x5A | kVK_ANSI_A - kVK_ANSI_Z |
|
||||
| 数字 0-9 | 0x30-0x39 | kVK_ANSI_0 - kVK_ANSI_9 |
|
||||
| 小键盘 | 0x60-0x69 | kVK_ANSI_Keypad0 - 9 |
|
||||
| F1-F12 | 0x70-0x7B | kVK_F1 - kVK_F12 |
|
||||
| 方向键 | 0x25-0x28 | kVK_LeftArrow 等 |
|
||||
| 修饰键 | 0x10-0x12 | kVK_Shift/Control/Option |
|
||||
| Win 键 | 0x5B-0x5C | kVK_Command |
|
||||
|
||||
### 3.4 差分算法(与 Linux 一致)
|
||||
|
||||
| 算法 | ID | 说明 |
|
||||
|-----|-----|------|
|
||||
| ALGORITHM_GRAY | 0 | 灰度压缩 |
|
||||
| ALGORITHM_DIFF | 1 | BGRA 差分 |
|
||||
| ALGORITHM_H264 | 2 | 硬件编码(未实现)|
|
||||
| ALGORITHM_RGB565 | 3 | RGB565 压缩 |
|
||||
|
||||
### 3.5 macOS 版本支持
|
||||
|
||||
| 版本 | 代号 | 发布年份 | 支持状态 |
|
||||
|-----|------|---------|---------|
|
||||
| 10.13 | High Sierra | 2017 | ✅ 目标最低版本 |
|
||||
| 10.14 | Mojave | 2018 | ✅ 支持 |
|
||||
| 10.15 | Catalina | 2019 | ✅ 支持(需权限) |
|
||||
| 11.x | Big Sur | 2020 | ✅ 支持 |
|
||||
| 12.x | Monterey | 2021 | ✅ 支持 |
|
||||
| 13.x | Ventura | 2022 | ✅ 支持 |
|
||||
| 14.x | Sonoma | 2023 | ✅ 支持 |
|
||||
|
||||
**注意**:macOS 10.15+ 需要用户授予屏幕录制权限。
|
||||
|
||||
---
|
||||
|
||||
## 4. 权限处理
|
||||
|
||||
### 4.1 所需权限
|
||||
|
||||
| 权限 | 用途 | 最低要求版本 | 授权方式 |
|
||||
|-----|------|------------|---------|
|
||||
| **屏幕录制** | 捕获屏幕内容 | 10.15 | 系统偏好设置 > 隐私 > 屏幕录制 |
|
||||
| **辅助功能** | 模拟键盘/鼠标输入 | 所有版本 | 系统偏好设置 > 隐私 > 辅助功能 |
|
||||
|
||||
### 4.2 权限检查实现(Permissions.mm)
|
||||
|
||||
```objc
|
||||
// 检查屏幕录制权限
|
||||
bool Permissions::checkScreenCapture() {
|
||||
if (@available(macOS 10.15, *)) {
|
||||
CGImageRef image = CGWindowListCreateImage(
|
||||
CGRectMake(0, 0, 1, 1),
|
||||
kCGWindowListOptionOnScreenOnly,
|
||||
kCGNullWindowID,
|
||||
kCGWindowImageDefault
|
||||
);
|
||||
if (image == nil) return false;
|
||||
CGImageRelease(image);
|
||||
return true;
|
||||
}
|
||||
return true; // 10.15 之前不需要权限
|
||||
}
|
||||
|
||||
// 检查辅助功能权限
|
||||
bool Permissions::checkAccessibility() {
|
||||
return AXIsProcessTrusted();
|
||||
}
|
||||
|
||||
// 请求辅助功能权限(弹出系统对话框)
|
||||
void Permissions::requestAccessibility() {
|
||||
NSDictionary *options = @{
|
||||
(__bridge id)kAXTrustedCheckOptionPrompt: @YES
|
||||
};
|
||||
AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options);
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 权限引导流程
|
||||
|
||||
```
|
||||
程序启动
|
||||
│
|
||||
▼
|
||||
检查屏幕录制权限 ──否──▶ 打开系统偏好设置
|
||||
│
|
||||
是
|
||||
│
|
||||
▼
|
||||
检查辅助功能权限 ──否──▶ 弹出授权对话框
|
||||
│
|
||||
是
|
||||
│
|
||||
▼
|
||||
正常运行
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 核心代码实现
|
||||
|
||||
### 5.1 ScreenHandler(已实现)
|
||||
|
||||
```objc
|
||||
// ScreenHandler.h
|
||||
class ScreenHandler {
|
||||
public:
|
||||
ScreenHandler();
|
||||
~ScreenHandler();
|
||||
|
||||
bool init(); // 初始化
|
||||
void start(IOCPClient* client, uint64_t id); // 开始捕获
|
||||
void stop(); // 停止捕获
|
||||
|
||||
int getWidth() const;
|
||||
int getHeight() const;
|
||||
|
||||
void sendBitmapInfo(); // 发送屏幕信息
|
||||
void onReceive(const uint8_t* data, size_t size);
|
||||
void applyQualityLevel(int8_t level, bool persist);
|
||||
|
||||
private:
|
||||
bool captureScreen(std::vector<uint8_t>& buffer); // 截屏
|
||||
void sendFirstScreen(); // 发送首帧
|
||||
void sendDiffFrame(); // 发送差分帧
|
||||
uint32_t compareBitmap(...); // 差分计算
|
||||
void convertBGRAtoGray(...); // 灰度转换
|
||||
void convertBGRAtoRGB565(...); // RGB565转换
|
||||
void captureLoop(); // 捕获循环
|
||||
|
||||
// 成员变量
|
||||
IOCPClient* m_client;
|
||||
CGDirectDisplayID m_displayID;
|
||||
BITMAPINFOHEADER_MAC m_bmpHeader;
|
||||
std::vector<uint8_t> m_prevFrame, m_currFrame;
|
||||
std::atomic<bool> m_running;
|
||||
std::atomic<uint8_t> m_algorithm;
|
||||
std::atomic<int> m_maxFPS;
|
||||
};
|
||||
```
|
||||
|
||||
### 5.2 InputHandler(已实现)
|
||||
|
||||
```objc
|
||||
// InputHandler.h
|
||||
class InputHandler {
|
||||
public:
|
||||
InputHandler();
|
||||
~InputHandler();
|
||||
|
||||
bool init(); // 初始化(检查权限)
|
||||
void handleInputEvent(const MSG64_MAC* msg); // 处理输入事件
|
||||
bool hasAccessibilityPermission() const;
|
||||
|
||||
private:
|
||||
void handleMouseMove(int x, int y);
|
||||
void handleMouseButton(CGMouseButton button, bool down, int x, int y);
|
||||
void handleMouseDoubleClick(CGMouseButton button, int x, int y);
|
||||
void handleMouseWheel(int delta);
|
||||
void handleKeyEvent(uint32_t vkCode, bool down);
|
||||
|
||||
static CGKeyCode vkToMacKeyCode(uint32_t vk); // VK 到 macOS 键码映射
|
||||
|
||||
bool m_hasPermission;
|
||||
CGPoint m_lastMousePos;
|
||||
bool m_leftButtonDown, m_rightButtonDown, m_middleButtonDown;
|
||||
};
|
||||
```
|
||||
|
||||
### 5.3 SystemManager(已实现)
|
||||
|
||||
```objc
|
||||
// SystemManager.h
|
||||
class SystemManager {
|
||||
public:
|
||||
SystemManager(IOCPClient* client, uint64_t clientID);
|
||||
~SystemManager();
|
||||
|
||||
void onReceive(const uint8_t* data, size_t size);
|
||||
|
||||
private:
|
||||
void sendProcessList(); // 进程列表
|
||||
void killProcesses(const uint8_t* data, size_t size);
|
||||
void sendWindowsList(); // 窗口列表
|
||||
|
||||
static std::vector<pid_t> getAllPids(); // 枚举进程
|
||||
static std::string getProcessName(pid_t pid);
|
||||
static std::string getProcessPath(pid_t pid);
|
||||
};
|
||||
```
|
||||
|
||||
### 5.4 main.mm(已实现)
|
||||
|
||||
主要功能:
|
||||
- 命令行参数解析
|
||||
- 权限检查和引导
|
||||
- 系统信息收集(主机名、OS版本、CPU、内存等)
|
||||
- 初始化各处理器
|
||||
- 主事件循环
|
||||
|
||||
```objc
|
||||
int main(int argc, const char* argv[]) {
|
||||
@autoreleasepool {
|
||||
// 1. 解析命令行参数
|
||||
// 2. 检查权限
|
||||
// 3. 收集系统信息
|
||||
// 4. 初始化 ScreenHandler, InputHandler
|
||||
// 5. 主循环
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 构建配置
|
||||
|
||||
### 6.1 CMakeLists.txt
|
||||
|
||||
```cmake
|
||||
cmake_minimum_required(VERSION 3.15)
|
||||
project(ghost_macos)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13")
|
||||
set(CMAKE_OSX_ARCHITECTURES "x86_64;arm64") # Universal Binary
|
||||
|
||||
set(SOURCES
|
||||
main.mm
|
||||
ScreenHandler.mm
|
||||
InputHandler.mm
|
||||
SystemManager.mm
|
||||
Permissions.mm
|
||||
)
|
||||
|
||||
add_executable(ghost ${SOURCES})
|
||||
|
||||
target_include_directories(ghost PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
# 链接 macOS 框架
|
||||
find_library(COCOA_FRAMEWORK Cocoa REQUIRED)
|
||||
find_library(COREGRAPHICS_FRAMEWORK CoreGraphics REQUIRED)
|
||||
find_library(IOKIT_FRAMEWORK IOKit REQUIRED)
|
||||
find_library(IOSURFACE_FRAMEWORK IOSurface REQUIRED)
|
||||
find_library(APPLICATIONSERVICES_FRAMEWORK ApplicationServices REQUIRED)
|
||||
find_library(SECURITY_FRAMEWORK Security REQUIRED)
|
||||
find_library(CARBON_FRAMEWORK Carbon REQUIRED)
|
||||
|
||||
target_link_libraries(ghost PRIVATE
|
||||
${COCOA_FRAMEWORK}
|
||||
${COREGRAPHICS_FRAMEWORK}
|
||||
${IOKIT_FRAMEWORK}
|
||||
${IOSURFACE_FRAMEWORK}
|
||||
${APPLICATIONSERVICES_FRAMEWORK}
|
||||
${SECURITY_FRAMEWORK}
|
||||
${CARBON_FRAMEWORK}
|
||||
)
|
||||
|
||||
target_compile_options(ghost PRIVATE -Wall -Wextra -fobjc-arc)
|
||||
```
|
||||
|
||||
### 6.2 编译命令
|
||||
|
||||
```bash
|
||||
# 方式 1: 使用脚本
|
||||
chmod +x build.sh
|
||||
./build.sh
|
||||
|
||||
# 方式 2: 手动编译
|
||||
mkdir build && cd build
|
||||
cmake ..
|
||||
make -j$(sysctl -n hw.ncpu)
|
||||
|
||||
# 输出: build/bin/ghost
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 实现进度
|
||||
|
||||
### Phase 1: 基础框架 ✅ 完成
|
||||
|
||||
| 任务 | 状态 | 产出 |
|
||||
|-----|------|------|
|
||||
| 创建目录结构 | ✅ | `macos/` 目录 |
|
||||
| CMakeLists.txt | ✅ | 构建配置 |
|
||||
| Permissions.h/mm | ✅ | 权限检查 |
|
||||
| ScreenHandler.h/mm | ✅ | 屏幕捕获 |
|
||||
| InputHandler.h/mm | ✅ | 输入模拟 |
|
||||
| SystemManager.h/mm | ✅ | 进程管理 |
|
||||
| main.mm | ✅ | 入口点 |
|
||||
| IOCPClient.h | ✅ | 网络 stub |
|
||||
|
||||
### Phase 2: 网络集成 ⏳ 待实现
|
||||
|
||||
| 任务 | 状态 | 说明 |
|
||||
|-----|------|------|
|
||||
| IOCPClient 实现 | ⏳ | 复用 client/IOCPClient |
|
||||
| 登录信息发送 | ⏳ | LOGIN_INFOR 结构 |
|
||||
| 心跳保活 | ⏳ | Heartbeat 机制 |
|
||||
| 命令分发 | ⏳ | DataProcess 回调 |
|
||||
|
||||
### Phase 3: 功能完善 ⏳ 待实现
|
||||
|
||||
| 任务 | 状态 |
|
||||
|-----|------|
|
||||
| 光标类型同步 | ⏳ |
|
||||
| 多显示器支持 | ⏳ |
|
||||
| 文件传输 | ⏳ |
|
||||
| 剪贴板同步 | ⏳ |
|
||||
|
||||
### Phase 4: 代码保护 ⏳ 待实现
|
||||
|
||||
| 任务 | 状态 |
|
||||
|-----|------|
|
||||
| 服务端密钥对生成 | ⏳ |
|
||||
| 服务端签名逻辑 | ⏳ |
|
||||
| 闭源库 libYamaCore | ⏳ |
|
||||
| 客户端绑定验证 | ⏳ |
|
||||
|
||||
---
|
||||
|
||||
## 8. 测试指南
|
||||
|
||||
### 8.1 编译前准备
|
||||
|
||||
```bash
|
||||
# 安装 Xcode Command Line Tools
|
||||
xcode-select --install
|
||||
|
||||
# 安装 CMake (通过 Homebrew)
|
||||
brew install cmake
|
||||
```
|
||||
|
||||
### 8.2 编译运行
|
||||
|
||||
```bash
|
||||
# 获取代码
|
||||
git clone <repo-url>
|
||||
cd YAMA/macos
|
||||
|
||||
# 编译
|
||||
./build.sh
|
||||
|
||||
# 运行
|
||||
./build/bin/ghost [server_ip] [port]
|
||||
```
|
||||
|
||||
### 8.3 首次运行权限授予
|
||||
|
||||
1. **屏幕录制权限**:
|
||||
- 系统偏好设置 > 隐私与安全性 > 屏幕录制
|
||||
- 添加 `ghost` 可执行文件
|
||||
|
||||
2. **辅助功能权限**:
|
||||
- 系统偏好设置 > 隐私与安全性 > 辅助功能
|
||||
- 添加 `ghost` 可执行文件
|
||||
|
||||
### 8.4 预期输出
|
||||
|
||||
```
|
||||
=== macOS Ghost Client (Phase 1) ===
|
||||
Server: 192.168.0.55:6543
|
||||
Checking permissions...
|
||||
Collecting system information...
|
||||
Hostname: MacBook-Pro.local
|
||||
OS: macOS 14.0.0
|
||||
CPU: Apple M1 (0 MHz, 8 cores)
|
||||
Memory: 16.0 GB
|
||||
User: username
|
||||
Resolution: 1:2560x1600
|
||||
Executable: /path/to/ghost
|
||||
Start Time: 2024-04-29 12:00:00
|
||||
Initializing handlers...
|
||||
Screen handler initialized: 2560x1600
|
||||
Input handler initialized
|
||||
=== Phase 1 Status ===
|
||||
Screen Capture: READY
|
||||
Input Control: READY
|
||||
Network: STUB (Phase 2)
|
||||
======================
|
||||
Starting main loop (Ctrl+C to exit)...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 常见问题
|
||||
|
||||
### 9.1 编译问题
|
||||
|
||||
| 问题 | 原因 | 解决方案 |
|
||||
|-----|------|---------|
|
||||
| 找不到 Foundation.h | 缺少 Xcode CLT | `xcode-select --install` |
|
||||
| CMake 找不到 | 未安装 CMake | `brew install cmake` |
|
||||
| 架构不匹配 | Universal Binary 问题 | 检查 CMAKE_OSX_ARCHITECTURES |
|
||||
|
||||
### 9.2 运行时问题
|
||||
|
||||
| 问题 | 原因 | 解决方案 |
|
||||
|-----|------|---------|
|
||||
| 程序无法打开 | 未签名 | `xattr -d com.apple.quarantine ghost` |
|
||||
| Screen Capture: NEED PERMISSION | 无录屏权限 | 系统偏好设置授权 |
|
||||
| Input Control: NEED PERMISSION | 无辅助功能权限 | 系统偏好设置授权 |
|
||||
|
||||
### 9.3 权限重置
|
||||
|
||||
```bash
|
||||
# 如果权限状态混乱,可重置
|
||||
tccutil reset ScreenCapture
|
||||
tccutil reset Accessibility
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 代码保护机制(待实现)
|
||||
|
||||
### 10.1 保护策略
|
||||
|
||||
核心思路:客户端内嵌服务端公钥,连接时验证服务端身份。
|
||||
|
||||
```
|
||||
┌─────────────────────┐ ┌─────────────────────┐
|
||||
│ macOS Client │ │ Your Server │
|
||||
│ │ 1. 挑战请求 │ │
|
||||
│ ┌─────────────────┐ │ ─────────────────▶ │ ┌─────────────────┐ │
|
||||
│ │ libYamaCore.a │ │ │ │ 服务端私钥 │ │
|
||||
│ │ (闭源) │ │ ◀───────────────── │ │ │ │
|
||||
│ │ 内嵌服务端公钥 │ │ 2. 签名响应 │ └─────────────────┘ │
|
||||
│ │ │ │ │ │
|
||||
│ │ 验证签名 ─────▶ 匹配 ──▶ 正常工作 │
|
||||
│ │ ─────▶ 不匹配 ──▶ 拒绝连接 │
|
||||
│ └─────────────────┘ │ │ │
|
||||
└─────────────────────┘ └─────────────────────┘
|
||||
```
|
||||
|
||||
### 10.2 实施步骤
|
||||
|
||||
1. 生成服务端密钥对
|
||||
2. 服务端添加签名逻辑
|
||||
3. 创建闭源库 libYamaCore
|
||||
4. 嵌入公钥编译
|
||||
5. 客户端集成验证
|
||||
|
||||
---
|
||||
|
||||
## 11. 参考资料
|
||||
|
||||
### 11.1 官方文档
|
||||
|
||||
- [CGDisplayCreateImage](https://developer.apple.com/documentation/coregraphics/1454434-cgdisplaycreateimage)
|
||||
- [CGEvent Reference](https://developer.apple.com/documentation/coregraphics/cgevent)
|
||||
- [Carbon Events.h](https://developer.apple.com/documentation/carbon/events_h)
|
||||
- [Accessibility API](https://developer.apple.com/documentation/applicationservices/axuielement_h)
|
||||
|
||||
### 11.2 开源项目参考
|
||||
|
||||
- [RustDesk macOS](https://github.com/rustdesk/rustdesk/tree/master/src/platform/macos)
|
||||
- [Sunshine Screen Capture](https://github.com/LizardByte/Sunshine/tree/master/src/platform/macos)
|
||||
|
||||
---
|
||||
|
||||
## 12. 修订历史
|
||||
|
||||
| 日期 | 版本 | 修改内容 |
|
||||
|-----|------|---------|
|
||||
| 2026-04-28 | 1.0 | 初始版本(设计文档) |
|
||||
| 2026-04-29 | 1.1 | 更新为实现文档,Phase 1 完成 |
|
||||
@@ -1638,11 +1638,14 @@ VOID CMy2015RemoteDlg::AddList(CString strIP, CString strAddr, CString strPCName
|
||||
return;
|
||||
}
|
||||
if (!ctx->GetClientData(ONLINELIST_IP).IsEmpty()) {
|
||||
Mprintf("上线消息 - 主机已经存在 [2]: %llu. IP= %s. Path= %s\n", id, data[ONLINELIST_IP], path);
|
||||
#ifndef _DEBUG
|
||||
Mprintf("上线消息 - 主机重复上线,暂存替补 [2]: %llu. IP= %s. Path= %s\n", id, data[ONLINELIST_IP], path);
|
||||
// 网络抖动导致客户端用新连接重复上线:这里不立即替换/关闭旧连接,
|
||||
// 否则旧连接被关会触发客户端反复重连抖动(更新前就一直在"替换旧连接")。
|
||||
// 改为把新连接暂存到替补列表,旧连接因心跳超时或断开被移除后再由
|
||||
// PromoteStandby 取替补转正。
|
||||
AddToStandbyList(ContextObject);
|
||||
LeaveCriticalSection(&m_cs);
|
||||
return;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1793,6 +1796,19 @@ VOID CMy2015RemoteDlg::ShowMessage(CString strType, CString strMsg)
|
||||
m_CList_Message.SetItemText(0, 2, strMsg);
|
||||
m_CList_Message.SetItemData(0, (DWORD_PTR)MsgColorLevel(strType, strMsg));
|
||||
|
||||
// 同步线程安全镜像,供 MCP get_audit_log 读取(CListCtrl 仅 UI 线程可见)。
|
||||
{
|
||||
EnterCriticalSection(&m_cs);
|
||||
LogMessage entry;
|
||||
entry.type = strType.GetString();
|
||||
entry.time = strTime.GetString();
|
||||
entry.msg = strMsg.GetString();
|
||||
m_MessageLog.insert(m_MessageLog.begin(), entry);
|
||||
if ((int)m_MessageLog.size() > MAX_MESSAGE_COUNT)
|
||||
m_MessageLog.pop_back();
|
||||
LeaveCriticalSection(&m_cs);
|
||||
}
|
||||
|
||||
CString strStatusMsg;
|
||||
|
||||
EnterCriticalSection(&m_cs);
|
||||
@@ -1872,6 +1888,18 @@ LRESULT CMy2015RemoteDlg::OnShowErrMessage(WPARAM wParam, LPARAM lParam)
|
||||
m_CList_Message.SetItemText(0, 1, strTime);
|
||||
m_CList_Message.SetItemText(0, 2, strText);
|
||||
m_CList_Message.SetItemData(0, (DWORD_PTR)MsgColorLevel(strTitle, strText));
|
||||
// 同步线程安全镜像,供 MCP get_audit_log 读取(CListCtrl 仅 UI 线程可见)。
|
||||
{
|
||||
EnterCriticalSection(&m_cs);
|
||||
LogMessage entry;
|
||||
entry.type = strTitle.GetString();
|
||||
entry.time = strTime.GetString();
|
||||
entry.msg = strText.GetString();
|
||||
m_MessageLog.insert(m_MessageLog.begin(), entry);
|
||||
if ((int)m_MessageLog.size() > MAX_MESSAGE_COUNT)
|
||||
m_MessageLog.pop_back();
|
||||
LeaveCriticalSection(&m_cs);
|
||||
}
|
||||
if(title)delete title;
|
||||
if(text)delete text;
|
||||
|
||||
@@ -3689,15 +3717,23 @@ void CMy2015RemoteDlg::CheckHeartbeat()
|
||||
for (context* ContextObject : toRemove) {
|
||||
auto host = ContextObject->GetAdditionalData(RES_CLIENT_PUBIP);
|
||||
host = host.IsEmpty() ? std::to_string(ContextObject->GetClientID()).c_str() : host;
|
||||
Mprintf("Client %s[%llu] heartbeat timeout!!! \n", host, ContextObject->GetClientID());
|
||||
if (m_needNotify)
|
||||
PostMessageA(WM_SHOWNOTIFY, (WPARAM)new CharMsg(_TR("主机掉线")),
|
||||
(LPARAM)new CharMsg(_TR("主机长时间无心跳: ") + host));
|
||||
PostMessageA(WM_SHOWMESSAGE, (WPARAM)new CharMsg(_TR("[主机下线] 主机长时间无心跳: ") + host), NULL);
|
||||
Mprintf("主机 %s[%llu]心跳超时\n", host, ContextObject->GetClientID());
|
||||
|
||||
int port = ContextObject->GetPort();
|
||||
RemoveFromHostList(ContextObject);
|
||||
uint64_t clientID = ContextObject->GetClientID();
|
||||
// 旧连接长期无心跳被移除:若有替补连接在等待,取替补转正。
|
||||
// 只有真正下线(无替补转正)时才提示"主机下线",避免替补接替时误报。
|
||||
bool promoted = RemoveFromHostList(ContextObject) && PromoteStandby(clientID);
|
||||
|
||||
if (!promoted) {
|
||||
Mprintf("Client %s[%llu] heartbeat timeout!!! \n", host, clientID);
|
||||
if (m_needNotify)
|
||||
PostMessageA(WM_SHOWNOTIFY, (WPARAM)new CharMsg(_TR("主机掉线")),
|
||||
(LPARAM)new CharMsg(_TR("主机长时间无心跳: ") + host));
|
||||
PostMessageA(WM_SHOWMESSAGE, (WPARAM)new CharMsg(_TR("[主机下线] 主机长时间无心跳: ") + host), NULL);
|
||||
Mprintf("主机 %s[%llu]心跳超时\n", host, clientID);
|
||||
} else {
|
||||
Mprintf("主机 %s[%llu]心跳超时,替补连接已转正接替\n", host, clientID);
|
||||
}
|
||||
|
||||
// 从待上线队列中移除(防止定时器访问已释放的 context)
|
||||
auto pit = std::find(m_PendingOnline.begin(), m_PendingOnline.end(), ContextObject);
|
||||
@@ -5148,6 +5184,12 @@ BOOL CALLBACK CMy2015RemoteDlg::OfflineProc(CONTEXT_OBJECT* ContextObject)
|
||||
|
||||
// Remove from host list and pending online queue
|
||||
info->hasLogin = g_2015RemoteDlg->RemoveFromHostList(ContextObject);
|
||||
// 无论断开的是主连接还是替补,都从替补列表里移除(幂等)。
|
||||
g_2015RemoteDlg->RemoveFromStandbyList(ContextObject);
|
||||
// 主连接被移除且有替补在等待时,立即取替补转正,避免主机短暂消失。
|
||||
if (info->hasLogin) {
|
||||
g_2015RemoteDlg->PromoteStandby(info->clientId);
|
||||
}
|
||||
auto& pending = g_2015RemoteDlg->m_PendingOnline;
|
||||
auto it = std::find(pending.begin(), pending.end(), (context*)ContextObject);
|
||||
if (it != pending.end()) {
|
||||
@@ -6652,6 +6694,70 @@ bool CMy2015RemoteDlg::RemoveFromHostList(context* ctx)
|
||||
return removed;
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::AddToStandbyList(CONTEXT_OBJECT* ctx)
|
||||
{
|
||||
if (!ctx) return;
|
||||
uint64_t clientID = ctx->GetClientID();
|
||||
auto it = m_StandbyHosts.find(clientID);
|
||||
if (it != m_StandbyHosts.end() && it->second != ctx) {
|
||||
// 已有旧替补:说明客户端又断线重连了,旧替补已失效,关闭它释放连接。
|
||||
Mprintf("替补列表:替换旧替补 [%llu]\n", clientID);
|
||||
it->second->CancelIO();
|
||||
}
|
||||
m_StandbyHosts[clientID] = ctx;
|
||||
Mprintf("替补列表:加入替补 [%llu],当前替补数=%zu\n", clientID, m_StandbyHosts.size());
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::RemoveFromStandbyList(CONTEXT_OBJECT* ctx)
|
||||
{
|
||||
if (!ctx) return;
|
||||
auto it = m_StandbyHosts.find(ctx->GetClientID());
|
||||
// 只有当前替补正是该 ctx 时才移除,避免误删更新后的替补(幂等)。
|
||||
if (it != m_StandbyHosts.end() && it->second == ctx) {
|
||||
m_StandbyHosts.erase(it);
|
||||
Mprintf("替补列表:移除替补 [%llu],当前替补数=%zu\n", ctx->GetClientID(), m_StandbyHosts.size());
|
||||
}
|
||||
}
|
||||
|
||||
bool CMy2015RemoteDlg::PromoteStandby(uint64_t clientID)
|
||||
{
|
||||
auto it = m_StandbyHosts.find(clientID);
|
||||
if (it == m_StandbyHosts.end())
|
||||
return false;
|
||||
|
||||
CONTEXT_OBJECT* standby = it->second;
|
||||
// 替补连接可能已被 RemoveStaleContext 标记移除(其 OfflineProc 正在等 m_cs),
|
||||
// 此时转正只会让主机短暂闪断后再次下线。跳过并清理,避免误转正已死的连接。
|
||||
if (standby->IsRemoved.load()) {
|
||||
m_StandbyHosts.erase(it);
|
||||
Mprintf("替补转正:主机 [%llu] 的替补连接已被移除,放弃转正\n", clientID);
|
||||
return false;
|
||||
}
|
||||
m_StandbyHosts.erase(it);
|
||||
|
||||
// 替补转正:加入主列表并更新索引(与 AddList 正常上线流程一致)。
|
||||
m_ClientIndex[clientID] = m_HostList.size();
|
||||
m_HostList.push_back(standby);
|
||||
if (WebService().IsRunning()) {
|
||||
WebService().MarkDeviceOnline(clientID);
|
||||
}
|
||||
m_PendingOnline.push_back(standby);
|
||||
|
||||
Mprintf("替补转正:主机 [%llu] 由替补连接接替上线\n", clientID);
|
||||
|
||||
// 重复上线时未补发过 MasterSettings,这里补发(含签名),让客户端完成登录配置。
|
||||
std::string signMessage(const std::string & privateKey, BYTE * msg, int len);
|
||||
CString startTime = standby->GetClientData(ONLINELIST_STARTTIME);
|
||||
MasterSettings copy = m_settings;
|
||||
std::string msg = startTime.GetString();
|
||||
msg += "|" + std::to_string(clientID);
|
||||
auto signature = signMessage("", (BYTE*)msg.c_str(), msg.length());
|
||||
ASSERT(signature.size() <= sizeof(copy.Signature));
|
||||
memcpy(copy.Signature, signature.data(), signature.size());
|
||||
SendMasterSettings(standby, copy);
|
||||
return true;
|
||||
}
|
||||
|
||||
LRESULT CMy2015RemoteDlg::OnUserOfflineMsg(WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
// OfflineProc already removed context from m_HostList/m_PendingOnline and added to m_PendingOffline.
|
||||
@@ -6661,23 +6767,24 @@ LRESULT CMy2015RemoteDlg::OnUserOfflineMsg(WPARAM wParam, LPARAM lParam)
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
// Show offline notification
|
||||
if (!info->ip.IsEmpty() && info->hasLogin) {
|
||||
// 判断主连接是否仍在线:OfflineProc 已在 IO 线程持锁内完成 RemoveFromHostList,
|
||||
// 若有替补连接被 PromoteStandby 转正,m_ClientIndex 里仍会有该 clientId。
|
||||
// 直接查 m_ClientIndex 比依赖 hasLogin 更稳健:既不受子连接 auth 改造影响,
|
||||
// 也能避免替补转正时误报"主机下线"。
|
||||
bool stillOnline = false;
|
||||
if (info->clientId != 0) {
|
||||
CLock L(m_cs);
|
||||
stillOnline = (m_ClientIndex.find(info->clientId) != m_ClientIndex.end());
|
||||
}
|
||||
|
||||
// Show offline notification(仅在主连接断开且无替补转正时才提示真正下线)
|
||||
if (!info->ip.IsEmpty() && info->hasLogin && !stillOnline) {
|
||||
ShowMessage(_TR("操作成功"), info->ip + " " + _TR("主机下线") + "[" + info->aliveInfo.c_str() + "]");
|
||||
Mprintf("%s 主机下线 [%s]\n", info->ip.GetString(), info->aliveInfo.c_str());
|
||||
}
|
||||
|
||||
// 关闭对应客户端的循环快照浮窗(如有)。CloseLoopTip 内部 find 找不到会静默返回。
|
||||
if (info->clientId != 0) {
|
||||
// 判断主连接是否仍在线:OfflineProc 已在 IO 线程持锁内完成 RemoveFromHostList,
|
||||
// 若 m_ClientIndex 里仍有该 clientId,说明还有另一条连接在列表中(即本次断开的
|
||||
// 是子连接),不应清理主连接的 UI 状态;反之说明主机真正下线。
|
||||
// 直接查 m_ClientIndex 比依赖 hasLogin 更稳健:不受未来子连接 auth 改造影响。
|
||||
bool stillOnline;
|
||||
{
|
||||
CLock L(m_cs);
|
||||
stillOnline = (m_ClientIndex.find(info->clientId) != m_ClientIndex.end());
|
||||
}
|
||||
if (!stillOnline) {
|
||||
// 主连接真正下线:关循环窗、释放缩略图 HBITMAP、清调度状态。
|
||||
CloseLoopTip(info->clientId);
|
||||
@@ -7093,7 +7200,10 @@ void CMy2015RemoteDlg::UpdateActiveWindow(CONTEXT_OBJECT* ctx)
|
||||
ctx->SetClientData(ONLINELIST_VIDEO, newVideo);
|
||||
changed = true;
|
||||
}
|
||||
id->SetLastHeartbeat(time(0));
|
||||
// 心跳必须刷新"实际发送者" ctx 的心跳,而不是按 clientID 查到的 id(主连接)。
|
||||
// 否则替补连接(ctx != id)的心跳会把主连接的心跳也刷新掉,导致主连接永不超时、
|
||||
// 替补永远无法转正。ctx 自身心跳已在函数前段记录,这里在锁内再做一次权威更新。
|
||||
ctx->SetLastHeartbeat(time(0));
|
||||
if (changed) {
|
||||
m_DirtyClients.insert(clientID);
|
||||
|
||||
@@ -9571,6 +9681,13 @@ void CMy2015RemoteDlg::OnMsglogDelete()
|
||||
for (auto it = selected.rbegin(); it != selected.rend(); ++it) {
|
||||
m_CList_Message.DeleteItem(*it);
|
||||
}
|
||||
// 同步线程安全镜像(新在前,索引与 CListCtrl 一一对应;同样倒序避免索引变化)
|
||||
EnterCriticalSection(&m_cs);
|
||||
for (auto it = selected.rbegin(); it != selected.rend(); ++it) {
|
||||
if (*it >= 0 && (size_t)*it < m_MessageLog.size())
|
||||
m_MessageLog.erase(m_MessageLog.begin() + *it);
|
||||
}
|
||||
LeaveCriticalSection(&m_cs);
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::OnMsglogCopy() {
|
||||
@@ -9608,6 +9725,10 @@ void CMy2015RemoteDlg::OnMsglogCopy() {
|
||||
void CMy2015RemoteDlg::OnMsglogClear()
|
||||
{
|
||||
m_CList_Message.DeleteAllItems();
|
||||
// 同步线程安全镜像,供 MCP get_audit_log 读取。
|
||||
EnterCriticalSection(&m_cs);
|
||||
m_MessageLog.clear();
|
||||
LeaveCriticalSection(&m_cs);
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::OnMsglogSearch()
|
||||
|
||||
@@ -340,10 +340,23 @@ public:
|
||||
std::vector<int> m_PendingOffline; // 存储端口号
|
||||
CListCtrlEx m_CList_Online;
|
||||
CListCtrl m_CList_Message;
|
||||
// 消息/审计日志的线程安全镜像(供 MCP get_audit_log 工具读取)。m_CList_Message 是
|
||||
// CListCtrl、只能在 UI 线程访问,MCP 线程据此镜像读取。受 m_cs 保护,新在前、旧在后,
|
||||
// 与 CListCtrl 显示顺序一致(索引一一对应,便于删除同步)。
|
||||
struct LogMessage {
|
||||
std::string type; // 类型(操作成功/主机上线/主机下线…)
|
||||
std::string time; // 时间戳 "YYYY-MM-DD HH:MM:SS"
|
||||
std::string msg; // 内容
|
||||
};
|
||||
std::vector<LogMessage> m_MessageLog;
|
||||
CSplitterBar m_SplitterBar;
|
||||
int m_nSplitPos = 160; // 消息区高度(像素),可拖动调整
|
||||
std::vector<context*> m_HostList; // 虚拟列表数据源(全部客户端)
|
||||
std::unordered_map<uint64_t, size_t> m_ClientIndex; // clientID -> m_HostList 索引映射
|
||||
// 重复上线的替补连接:clientID -> 替补 context。
|
||||
// 网络抖动导致客户端用新连接重复上线时,不立即替换/关闭旧连接(否则会触发客户端
|
||||
// 反复重连抖动),而是把新连接暂存到这里;旧连接因心跳超时或断开被移除后,再取替补转正。
|
||||
std::unordered_map<uint64_t, CONTEXT_OBJECT*> m_StandbyHosts;
|
||||
std::vector<size_t> m_FilteredIndices; // 当前分组过滤后的索引列表
|
||||
int m_nSortColumn = -1; // 当前排序列,-1 表示未排序
|
||||
bool m_bSortAscending = true; // 排序方向(true=升序)
|
||||
@@ -366,6 +379,9 @@ public:
|
||||
context* FindHostNoLock(int port); // caller must hold m_cs lock
|
||||
context* FindHostNoLock(uint64_t id); // caller must hold m_cs lock
|
||||
bool RemoveFromHostList(context* ctx); // 从 m_HostList 中移除并更新索引
|
||||
void AddToStandbyList(CONTEXT_OBJECT* ctx); // 重复上线:暂存替补连接(替换旧替补)
|
||||
void RemoveFromStandbyList(CONTEXT_OBJECT* ctx); // 连接断开时从替补列表移除(幂等)
|
||||
bool PromoteStandby(uint64_t clientID); // 旧连接移除后,取替补转正(需持有 m_cs);返回是否真的转正
|
||||
|
||||
CStatusBar m_StatusBar; //状态条
|
||||
ULONGLONG m_ullStartTime = 0; // 程序启动时间 (GetTickCount64)
|
||||
|
||||
@@ -639,6 +639,34 @@ Json::Value BuildClientLogOutputSchema() {
|
||||
return schema;
|
||||
}
|
||||
|
||||
// get_audit_log 的 outputSchema(服务端消息/审计日志条目数组)
|
||||
Json::Value BuildAuditLogOutputSchema() {
|
||||
Json::Value props(Json::objectValue);
|
||||
|
||||
Json::Value entriesProp(Json::objectValue);
|
||||
entriesProp["type"] = "array";
|
||||
Json::Value items(Json::objectValue);
|
||||
items["type"] = "object";
|
||||
Json::Value itemProps(Json::objectValue);
|
||||
const char* strFields[] = { "type", "time", "msg" };
|
||||
for (const char* f : strFields) {
|
||||
Json::Value s(Json::objectValue);
|
||||
s["type"] = "string";
|
||||
itemProps[f] = s;
|
||||
}
|
||||
items["properties"] = itemProps;
|
||||
entriesProp["items"] = items;
|
||||
props["entries"] = entriesProp;
|
||||
|
||||
Json::Value schema(Json::objectValue);
|
||||
schema["type"] = "object";
|
||||
schema["properties"] = props;
|
||||
Json::Value required(Json::arrayValue);
|
||||
required.append("entries");
|
||||
schema["required"] = required;
|
||||
return schema;
|
||||
}
|
||||
|
||||
// get_screenshot 的 outputSchema(image 元数据;base64 数据在 content 的 image 块中)
|
||||
Json::Value BuildScreenshotOutputSchema() {
|
||||
Json::Value props(Json::objectValue);
|
||||
@@ -897,6 +925,23 @@ std::string BuildToolsListResult(const Json::Value& id) {
|
||||
tools.append(tool);
|
||||
}
|
||||
|
||||
// 11) get_audit_log(服务端本地:主界面消息/审计日志,纯内存,无子链接)
|
||||
{
|
||||
Json::Value tool(Json::objectValue);
|
||||
tool["name"] = "get_audit_log";
|
||||
tool["description"] = u8"获取 YAMA 服务端主界面的消息/审计日志(主机上线/下线、操作结果、告警等)。返回列表内当前全部条目(最多 1000 条,新在前)。只读、纯内存。";
|
||||
|
||||
Json::Value inputSchema(Json::objectValue);
|
||||
inputSchema["type"] = "object";
|
||||
inputSchema["properties"] = Json::Value(Json::objectValue);
|
||||
inputSchema["required"] = Json::Value(Json::arrayValue);
|
||||
tool["inputSchema"] = inputSchema;
|
||||
|
||||
tool["outputSchema"] = BuildAuditLogOutputSchema();
|
||||
|
||||
tools.append(tool);
|
||||
}
|
||||
|
||||
result["tools"] = tools;
|
||||
return BuildResult(id, result);
|
||||
}
|
||||
@@ -1418,6 +1463,41 @@ std::string BuildGetClientLog(const Json::Value& id, const Json::Value& args, CM
|
||||
return BuildResult(id, result);
|
||||
}
|
||||
|
||||
// tools/call:get_audit_log(服务端本地消息日志,读取 UI 日志列表的线程安全镜像)
|
||||
std::string BuildGetAuditLog(const Json::Value& id, CMy2015RemoteDlg* parent) {
|
||||
Json::Value entries(Json::arrayValue);
|
||||
if (parent) {
|
||||
EnterCriticalSection(&parent->m_cs);
|
||||
// m_MessageLog 新在前、旧在后(与界面一致),直接顺序输出。
|
||||
for (const auto& e : parent->m_MessageLog) {
|
||||
Json::Value item(Json::objectValue);
|
||||
item["type"] = ToUtf8(e.type.c_str(), 936);
|
||||
item["time"] = ToUtf8(e.time.c_str(), 936);
|
||||
item["msg"] = ToUtf8(e.msg.c_str(), 936);
|
||||
entries.append(item);
|
||||
}
|
||||
LeaveCriticalSection(&parent->m_cs);
|
||||
}
|
||||
|
||||
int count = (int)entries.size();
|
||||
|
||||
Json::Value result(Json::objectValue);
|
||||
Json::Value structuredContent(Json::objectValue);
|
||||
structuredContent["entries"] = entries;
|
||||
result["structuredContent"] = structuredContent;
|
||||
|
||||
Json::Value content(Json::arrayValue);
|
||||
Json::Value item(Json::objectValue);
|
||||
item["type"] = "text";
|
||||
item["text"] = count > 0 ? std::string(u8"共 ") + std::to_string(count) + std::string(u8" 条日志。")
|
||||
: std::string(u8"当前无日志。");
|
||||
content.append(item);
|
||||
result["content"] = content;
|
||||
result["isError"] = false;
|
||||
|
||||
return BuildResult(id, result);
|
||||
}
|
||||
|
||||
// tools/call 分派
|
||||
std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
|
||||
const Json::Value& id = root["id"];
|
||||
@@ -1440,6 +1520,7 @@ std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
|
||||
if (toolName == "list_files") return BuildListFiles(id, args, parent);
|
||||
if (toolName == "list_services") return BuildListServices(id, args, parent);
|
||||
if (toolName == "get_client_log") return BuildGetClientLog(id, args, parent);
|
||||
if (toolName == "get_audit_log") return BuildGetAuditLog(id, parent);
|
||||
|
||||
return BuildError(id, -32602,
|
||||
"Unknown tool: " + (toolName.empty() ? std::string("(empty)") : toolName));
|
||||
|
||||
Reference in new issue
Block a user