Task configuration:
{"task_id": "06", "task_name": "render_map_image", "description": "\u5728QGIS\u4e2d\u5bfc\u51fa\u5730\u56fe\u56fe\u7247", "application_info": {"name": "QGIS", "executable_path": "/home/agent/qgis/QGIS/build-master/output/bin/qgis", "args": []}, "mcp_servers": [{"command": "python", "args": ["/home/agent/agent-demo/PC-Canary/tests/context_data/QGIS/qgis_config/qgis_mcp/qgis_mcp_server.py"]}], "context_data": [{"from": "tests/context_data/QGIS/qgis_sample_data", "to": "/home/agent/testbed/QGIS"}, {"from": "tests/context_data/QGIS/qgis_config", "to": "/home/agent/.local/share/QGIS"}], "task_parameters": {"save_path": "/home/agent/testbed/QGIS/test.png", "type": "png", "width": "800", "height": "600"}, "instruction_template": "\u5728QGIS\u4e2d\u4ee5\u56fe\u7247\u683c\u5f0f\u5bfc\u51fa\u5730\u56fe,\u56fe\u7247\u7684\u5bbd\u4e3a${width}px,\u9ad8\u4e3a${height}px,\u56fe\u7247\u7684\u4fdd\u5b58\u8def\u5f84\u4e3a${save_path},\u7c7b\u578b\u4e3a${type}", "evaluation_setup": {"scripts": [{"role": "hook", "path": "hooker.js"}, {"role": "handler", "path": "handler.py"}], "timeout": 180}, "total_key_steps": 2, "events": {"script_initialized": {"description": "\u94a9\u5b50\u811a\u672c\u521d\u59cb\u5316", "is_key_step": false}, "setPathandType_function_found": {"description": "\u627e\u5230\u8bbe\u7f6e\u5bfc\u51fa\u56fe\u7247\u8def\u5f84\u548c\u7c7b\u578b\u7684\u51fd\u6570", "is_key_step": false}, "PathandType_set": {"description": "\u5bfc\u51fa\u56fe\u7247\u8def\u5f84\u548c\u7c7b\u578b\u88ab\u6b63\u786e\u8bbe\u7f6e", "is_key_step": true, "key_step_index": 1, "key_step_name": "\u56fe\u7247\u8def\u5f84\u548c\u7c7b\u578b\u6b63\u786e"}, "setSize_function_found": {"description": "\u627e\u5230\u8bbe\u7f6e\u5bfc\u51fa\u56fe\u7247\u5c3a\u5bf8\u7684\u51fd\u6570", "is_key_step": false}, "Size_set": {"description": "\u5bfc\u51fa\u56fe\u7247\u5c3a\u5bf8\u88ab\u6b63\u786e\u8bbe\u7f6e", "is_key_step": true, "key_step_index": 2, "key_step_name": "\u56fe\u7247\u5c3a\u5bf8\u6b63\u786e"}, "error": {"description": "\u6765\u81ea\u811a\u672c\u7684\u9519\u8bef\u4e8b\u4ef6", "is_key_step": false}}}

Hook script:
// QGIS导出地图图像钩子脚本
// 用于监听QGIS导出地图为图片的相关操作并检测参数

(function() {
    // 脚本常量设置
    const setPathandType_SYMBOL_NAME="_ZN11QgsGuiUtils18getSaveAsImageNameEP7QWidgetRK7QStringS4_" // QgsGuiUtils::getSaveAsImageName 函数的符号
    const setPathandType_SYMBOL_NAME2="_ZNK6QImage4saveERK7QStringPKci" // QImage::save 函数的符号
    const setSize_SYMBOL_NAME="_ZN14QgsMapSettings13setOutputSizeE5QSize" // QgsMapSettings::setOutputSize 函数的符号    
    // 向评估系统发送事件
    function sendEvent(eventType, data = {}) {
        const payload = {
            event: eventType,
            ...data,
            timestamp: new Date().getTime()
        };
        send(payload);
    }
    
    const HEADER_SIZE = 24;   
    const SIZE_OFFSET = 4;                  

    function qstringToString(qstr) {
        try {
            const d = qstr.readPointer();                         // QString::d
            const len = d.add(SIZE_OFFSET).readU32();             // QStringData::size
            const data = d.add(HEADER_SIZE);                      // first UTF‑16 char
            return Memory.readUtf16String(data, len);
        } catch (error) {
            console.log("解析QString失败:", error);
            return "";
        }
    }

    // 初始化钩子并立即执行
    function initHook() {
        sendEvent("script_initialized", {
            message: "QGIS导出地图图像监控脚本已启动"
        });
        
        // 查找设置路径和类型的函数
        let setPathandTypeAddr = Module.findExportByName(null, setPathandType_SYMBOL_NAME);
        
        // 如果没找到，尝试扫描所有加载的模块
        if (!setPathandTypeAddr) {
            sendEvent("function_search_start", {
                message: "正在查找QgsGuiUtils::getSaveAsImageName函数..."
            });
            
            // 遍历模块
            Process.enumerateModules({
                onMatch: function(module) {
                    if (module.name.includes("qgis_gui")) {
                        sendEvent("module_found", {
                            module_name: module.name,
                            base_address: module.base.toString()
                        });
                        
                        // 在qgis模块中查找符号
                        const symbol = module.findExportByName(setPathandType_SYMBOL_NAME);
                        if (symbol) {
                            setPathandTypeAddr = symbol;
                        }
                    }
                },
                onComplete: function() {}
            });
            
            // 如果仍未找到，报告错误
            if (!setPathandTypeAddr) {
                sendEvent("error", {
                    error_type: "function_not_found",
                    message: "无法找到QgsGuiUtils::getSaveAsImageName函数"
                });
            }
        }
        
        // 如果找到了第一个函数，报告并安装钩子
        if (setPathandTypeAddr) {
            // 报告找到函数
            sendEvent("setPathandType_function_found", {
                address: setPathandTypeAddr.toString(),
                message: "找到QgsGuiUtils::getSaveAsImageName函数"
            });
            
            // 安装钩子 - 设置路径和类型
            Interceptor.attach(setPathandTypeAddr, {
                onLeave: function(retval) {
                    try {
                        // 获取QPair<QString, QString>的返回值
                        const pairPtr = retval;
                        
                        // 读取第一个QString - 文件路径
                        const pathQString = pairPtr;
                        const filePath = qstringToString(pathQString);
                        
                        // 读取第二个QString - 文件类型
                        const typeQString = pairPtr.add(Process.pointerSize); // QString的大小通常是指针大小的4倍
                        const fileType = qstringToString(typeQString);
                        
                        console.log("导出图片路径:", filePath, "类型:", fileType);
                        
                        // 发送事件通知
                        sendEvent("PathandType_set", {
                            path: filePath,
                            type: fileType,
                            message: `检测到导出图片设置: 路径=${filePath}, 类型=${fileType}`
                        });
                    } catch (error) {
                        sendEvent("error", {
                            error_type: "hook_execution_error",
                            message: `获取路径和类型时出错: ${error.message}`,
                            stack: error.stack
                        });
                    }
                }
            });
        }
        
        // 查找QImage::save函数
        let imageSaveAddr = Module.findExportByName(null, setPathandType_SYMBOL_NAME2);
        
        // 如果没找到，尝试扫描所有加载的模块
        if (!imageSaveAddr) {
            sendEvent("function_search_start", {
                message: "正在查找QImage::save函数..."
            });
            
            // 遍历模块
            Process.enumerateModules({
                onMatch: function(module) {
                    if (module.name.includes("Qt5")) {
                        sendEvent("module_found", {
                            module_name: module.name,
                            base_address: module.base.toString()
                        });
                        
                        // 在Qt5模块中查找符号
                        const symbol = module.findExportByName(setPathandType_SYMBOL_NAME2);
                        if (symbol) {
                            imageSaveAddr = symbol;
                        }
                    }
                },
                onComplete: function() {}
            });
            
            // 如果仍未找到，报告错误
            if (!imageSaveAddr) {
                sendEvent("error", {
                    error_type: "function_not_found",
                    message: "无法找到QImage::save函数"
                });

            }
        }
        
        // 如果找到了QImage::save函数，报告并安装钩子
        if (imageSaveAddr) {
            // 报告找到函数
            sendEvent("image_save_function_found", {
                address: imageSaveAddr.toString(),
                message: "找到QImage::save函数"
            });
            
            // 安装钩子 - QImage::save
            Interceptor.attach(imageSaveAddr, {
                onEnter: function(args) {
                    try {
                        // 获取第一个参数 - 文件路径（const QString &fileName）
                        const pathQString = args[1]; // 
                        const filePath = qstringToString(pathQString);
                        
                        // 从文件路径获取扩展名作为类型
                        let fileType = "";
                        const lastDotIndex = filePath.lastIndexOf('.');
                        if (lastDotIndex !== -1 && lastDotIndex < filePath.length - 1) {
                            fileType = filePath.substring(lastDotIndex + 1).toLowerCase();
                        }
                        
                        console.log("QImage保存图片路径:", filePath, "类型:", fileType);
                        
                        // 发送事件通知
                        sendEvent("PathandType_set", {
                            path: filePath,
                            type: fileType,
                            message: `检测到QImage保存图片: 路径=${filePath}, 类型=${fileType}`
                        });
                    } catch (error) {
                        sendEvent("error", {
                            error_type: "hook_execution_error",
                            message: `获取QImage::save路径和类型时出错: ${error.message}`,
                            stack: error.stack
                        });
                    }
                }
            });
        }
        
        // 查找设置尺寸的函数
        let setSizeAddr = Module.findExportByName(null, setSize_SYMBOL_NAME);
        
        // 如果没找到，尝试扫描所有加载的模块
        if (!setSizeAddr) {
            sendEvent("function_search_start", {
                message: "正在查找QgsMapSettings::setOutputSize函数..."
            });
            
            // 遍历模块
            Process.enumerateModules({
                onMatch: function(module) {
                    if (module.name.includes("qgis_core")) {
                        sendEvent("module_found", {
                            module_name: module.name,
                            base_address: module.base.toString()
                        });
                        
                        // 在qgis模块中查找符号
                        const symbol = module.findExportByName(setSize_SYMBOL_NAME);
                        if (symbol) {
                            setSizeAddr = symbol;
                        }
                    }
                },
                onComplete: function() {}
            });
            
            // 如果仍未找到，报告错误
            if (!setSizeAddr) {
                sendEvent("error", {
                    error_type: "function_not_found",
                    message: "无法找到QgsMapSettings::setOutputSize函数"
                });
                return;
            }
        }
        
        // 报告找到函数
        sendEvent("setSize_function_found", {
            address: setSizeAddr.toString(),
            message: "找到QgsMapSettings::setOutputSize函数"
        });
        
        // 安装钩子 - 设置尺寸
        Interceptor.attach(setSizeAddr, {
            onEnter: function(args) {
                try {
                    //按照 System V AMD64 ABI 的第 3.2.3 节，对于小于或等于 16 字节的结构体，如果结构体所有字段均属于整型或指针，
                    //则整个结构体被划分为一个或多个 8 字节块，每块均归入 INTEGER 类，通过整数寄存器传递。
                    // 先把 NativePointer 转为无符号 64 位 BigInt
                    // 1. 拿到十六进制字符串，去掉前缀 "0x"
                    let hex = args[1].toString(16);
                    if (hex.startsWith("0x")) { hex = hex.slice(2); }

                    // 2. 补全到 16 位（8 字节）长度
                    hex = hex.padStart(16, "0");

                    // 3. 高 8 字节 = height，低 8 字节 = width
                    const hi = hex.slice(0, 8);
                    const lo = hex.slice(8);

                    const height = parseInt(hi, 16);
                    const width  = parseInt(lo, 16);
                    
                    console.log("导出图片尺寸: 宽=", width, "高=", height);
                    
                    // 发送事件通知
                    sendEvent("Size_set", {
                        width: width,
                        height: height,
                        message: `检测到图片尺寸设置: 宽=${width}px, 高=${height}px`
                    });
                } catch (error) {
                    sendEvent("error", {
                        error_type: "hook_execution_error",
                        message: `获取尺寸时出错: ${error.message}`,
                        stack: error.stack
                    });
                }
            }
        });
        
        sendEvent("hook_installed", {
            message: "钩子安装完成，等待导出地图操作..."
        });
    }
    
    // 立即执行钩子初始化
    initHook();
})();

Evaluation handler:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
QGIS导出地图图像事件处理器
负责处理钩子脚本产生的事件并更新评估指标
"""

import os
import re
from typing import Dict, Any, Optional, List

def message_handler(message: Dict[str, Any], logger: Any, task_parameter: Dict[str, Any]) -> Optional[List[Dict[str, Any]]]:
    """
    处理从钩子脚本接收的消息
    
    Args:
        message: Frida消息对象
        logger: 日志记录器
        task_parameter: 任务参数
        
    Returns:
        Optional[List[Dict[str, Any]]]: 如果任务状态有更新返回状态列表，否则返回None
    """

    # 处理消息
    if message.get('type') == 'send' and 'payload' in message:
        payload = message['payload']
        
        # 检查是否包含事件类型
        if 'event' in payload:
            event_type = payload['event']
            logger.debug(f"接收到事件: {event_type}")
            
            # 处理特定事件
            if event_type == "script_initialized":
                logger.info(f"钩子脚本初始化: {payload.get('message', '')}")
                return None
                
            elif event_type == "setPathandType_function_found":
                logger.info(f"找到设置导出路径和类型函数: {payload.get('address', '')}")
                return None
                
            elif event_type == "setSize_function_found":
                logger.info(f"找到设置尺寸函数: {payload.get('address', '')}")
                return None
                
            elif event_type == "PathandType_set":
                path = payload.get("path", "")
                file_type = payload.get("type", "")
                logger.info(f"检测到导出图片设置: 路径={path}, 类型={file_type}")
                
                # 检查路径是否匹配预期
                expected_path = task_parameter.get("save_path", "").lower()
                expected_type = task_parameter.get("type", "").lower()
                
                path_matches = expected_path in path.lower() or os.path.basename(expected_path) in path.lower()
                
                # 类型可能包含在过滤器字符串中，如"PNG Files (*.png)"
                type_matches = expected_type in file_type.lower()
                
                if path_matches and type_matches:
                    logger.info("导出路径和类型匹配预期目标!")                    
                    # 返回关键步骤
                    return [{"status": "key_step", "index": 1, "name": "图片路径和类型正确"}]
                else:
                    logger.info(f"导出设置与预期不符。预期路径: {expected_path}, 实际: {path}; 预期类型: {expected_type}, 实际: {file_type}")
                
            elif event_type == "Size_set":
                width = payload.get("width", 0)
                height = payload.get("height", 0)
                logger.info(f"检测到导出图片尺寸: 宽={width}px, 高={height}px")
                
                # 检查尺寸是否匹配预期
                expected_width = int(task_parameter.get("width", "0"))
                expected_height = int(task_parameter.get("height", "0"))
                
                if width == expected_width and height == expected_height:
                    logger.info("导出尺寸匹配预期目标!")                    
                    # 返回关键步骤和成功状态（全部步骤完成）
                    return [
                        {"status": "key_step", "index": 2, "name": "图片尺寸正确"},
                        {"status": "success", "reason": f"成功导出地图图像: 路径匹配, 尺寸={width}x{height}"}
                    ]
                else:
                    logger.info(f"导出尺寸与预期不符。预期: {expected_width}x{expected_height}, 实际: {width}x{height}")
                
            elif event_type == "error":
                error_type = payload.get("error_type", "unknown")
                error_message = payload.get("message", "未知错误")
                logger.error(f"钩子脚本错误 ({error_type}): {error_message}")
                
                # 返回错误状态
                return [{
                    "status": "error",
                    "type": error_type,
                    "message": error_message
                }]
                
    elif message.get('type') == 'error':
        stack = message.get('stack', '')
        logger.error(f"钩子脚本错误: {stack}")
        
        # 返回错误状态
        return [{
            "status": "error",
            "type": "script_error",
            "message": "钩子脚本执行错误",
            "stack_trace": stack
        }]
    
    return None