Task configuration:
{"task_id": "17", "task_name": "create_disk_with_threaded_hole", "description": "Create a disk with a threaded hole in FreeCAD", "application_info": {"name": "FreeCAD", "executable_path": "/usr/local/bin/FreeCAD", "args": []}, "task_parameters": {"source_path": "/FreeCAD/", "filename": "task17.FCStd", "disk_radius": 50.0, "disk_height": 10.0, "thread_size": "M10", "thread_depth": 10.0, "model_thread": true}, "instruction_template": "The application is already open. Create a disk in FreeCAD with radius ${disk_radius}mm and height ${disk_height}mm, then add a threaded hole in its center with the following parameters:\n- Profile: ISO metric regular profile\n- Size: ${thread_size}\n- Clearance: Standard\n- Depth: ${thread_depth}mm\n- Threaded: checked\n- Model Thread: ${model_thread ? 'checked' : 'unchecked'}\nThe disk should be created as an additive cylinder (typeid), and the threaded hole should be created using the create a threaded hole (typeid) method. Do not create the shapes separately and use boolean subtraction or cut. Finally, save the file to ${source_path}${filename}", "evaluation_setup": {"scripts": [{"role": "hook", "path": "hooker.js"}, {"role": "handler", "path": "handler.py"}], "timeout": 300, "evaluator_type": "HookManager"}, "preconditions": {"freecad_running": false}, "total_key_steps": 2, "events": {"script_initialized": {"description": "Hook script initialized", "is_key_step": false}, "function_found": {"description": "Document save function found", "is_key_step": false}, "function_called": {"description": "Document save function called", "is_key_step": true, "key_step_index": 1, "key_step_name": "Document saved"}, "function_key_word_detected": {"description": "Detected key content in saved document", "is_key_step": true, "key_step_index": 2, "key_step_name": "Created disk with threaded hole and saved successfully"}, "error": {"description": "Error event", "is_key_step": false}}, "mcp_servers": [{"command": "python3", "args": ["/home/agent/.local/share/FreeCAD/Mod/freecad_mcp/src/freecad_bridge.py"]}]}

Hook script:
// FreeCAD Disk with Threaded Hole Monitoring Hook Script
// Used to monitor FreeCAD operations for creating a disk with threaded hole and detect task completion
// After creating a disk with threaded hole and saving the file, the test program detects the save and checks if the document contains a disk with threaded hole that meets requirements

(function() {
  // Script constants setup
  const FUNCTION_NAME = "_ZNK3App8Document10saveToFileEPKc"
  const ORIGIN_FUNCTION_NAME = "Document::saveToFile"
  const FUNCTION_BEHAVIOR = "Save document"

  const SCRIPT_INITIALIZED = "script_initialized"
  const FUNCTION_NOT_FOUND = "function_not_found"
  const FUNCTION_FOUND = "function_found"
  const FUNCTION_CALLED = "function_called"
  const FUNCTION_KEY_WORD_DETECTED = "function_key_word_detected"
  const ERROR = "error"
  const HOOK_INSTALLED = "hook_installed"

  const APP_NAME = "FreeCAD"
  
  // Global variables
  let funcFound = false;
  
  // Send event to evaluation system
  function sendEvent(eventType, data = {}) {
      const payload = {
          event: eventType,
          ...data,
          timestamp: new Date().getTime()
      };
      send(payload);
  }
  
  // Find Document::saveToFile function
  function getFunction() {
      // Try to find directly through exported symbol
      let FuncAddr = DebugSymbol.getFunctionByName(FUNCTION_NAME);
      
      // If not found, report error
      if (!FuncAddr) {
          sendEvent(ERROR, {
              error_type: FUNCTION_NOT_FOUND,
              message: `Cannot find ${ORIGIN_FUNCTION_NAME} function`
          });
          return null;
      }
      
      // Report function found
      funcFound = true;
      sendEvent(FUNCTION_FOUND, {
          address: FuncAddr.toString(),
          message: `Found ${ORIGIN_FUNCTION_NAME} function`
      });
      
      return FuncAddr;
  }
  
  // Initialize hook and execute immediately
  function initHook() {
      sendEvent(SCRIPT_INITIALIZED, {
          message: `${APP_NAME} ${FUNCTION_BEHAVIOR} monitoring script started`
      });
      
      // Find search function
      const funcAddr = getFunction();
      if (!funcAddr) {
          return;
      }
      
      // Install search function hook
      Interceptor.attach(funcAddr, {
          onEnter: function(args) {
              try {
                  sendEvent(FUNCTION_CALLED, {
                      message: `Intercepted ${FUNCTION_BEHAVIOR} function call`
                  });
                  this.filename = args[1].readCString();
              } catch (error) {
                  sendEvent(ERROR, {
                      error_type: "general_error",
                      message: `Execution error: ${error.message}`
                  });
              }
          },

          onLeave: function(retval) {
              try {
                if (retval) {
                    const pythonCode = `
import freecad
import FreeCAD
import Part
import math
from FreeCAD import Vector

# Open the specified file
file_path = '${this.filename}'
doc = FreeCAD.open(file_path)

# Get active document
if doc is None:
    result = None
else:
    # Initialize result parameters
    disk_radius = 0.0
    disk_height = 0.0
    hole_radius = 0.0
    has_thread = False
    is_centered = False
    
    # Initialize threaded hole parameters
    thread_size = ''
    thread_depth = 0.0
    model_thread = False
    
    # Find disk and threaded hole
    cylinder_found = False
    hole_found = False
    disk_center = Vector(0, 0, 0)
    
    # 检查所有对象，寻找圆盘和螺纹孔
    for obj in doc.Objects:
        # 检查是否为实体对象
        if hasattr(obj, "Shape"):
            # 检查形状类型
            if hasattr(obj.Shape, "ShapeType"):
                # Part设计方式创建的对象
                if obj.TypeId == "PartDesign::Body":
                    for subobj in obj.OutList:
                        if hasattr(subobj, "Shape") and hasattr(subobj.Shape, "ShapeType"):
                            # Check cylinder/disk
                            if subobj.TypeId == 'PartDesign::AdditiveCylinder' or subobj.TypeId == 'Part::Cylinder':
                                if not cylinder_found:  # Only process the first disk found
                                    cylinder_found = True
                                    if hasattr(subobj, "Radius"):
                                        disk_radius = subobj.Radius.Value
                                    elif hasattr(subobj.Shape, "Radius"):
                                        disk_radius = subobj.Shape.Radius
                                    
                                    if hasattr(subobj, "Height"):
                                        disk_height = subobj.Height.Value
                                    elif hasattr(subobj.Shape, "Height"):
                                        disk_height = subobj.Shape.Height
                                    
                                    # Record disk center position
                                    if hasattr(subobj, "Placement"):
                                        disk_center = subobj.Placement.Base
                            
                            # Check threaded hole
                            if 'Hole' in subobj.TypeId or 'Thread' in subobj.TypeId:
                                hole_found = True
                                has_thread = True
                                
                                # Only get key parameters that need to be verified
                                
                                # Thread Size
                                if hasattr(subobj, "ThreadSize") or hasattr(subobj, "Size"):
                                    size = getattr(subobj, "ThreadSize", None) or getattr(subobj, "Size", None)
                                    if size:
                                        if isinstance(size, str):
                                            thread_size = size
                                        else:
                                            # If it's a dimension object, try to get the value and convert to string
                                            try:
                                                thread_size = 'M' + str(int(size.Value))
                                            except:
                                                # If parsing fails, keep empty
                                                pass
                                
                                # Get hole radius (to check if centered)
                                if hasattr(subobj, "Diameter"):
                                    hole_radius = subobj.Diameter.Value / 2.0
                                elif hasattr(subobj, "ThreadSize") and not isinstance(subobj.ThreadSize, str):
                                    hole_radius = subobj.ThreadSize.Value / 2.0
                                elif hasattr(subobj, "Radius"):
                                    hole_radius = subobj.Radius.Value
                                
                                # Depth
                                if hasattr(subobj, "Depth"):
                                    thread_depth = subobj.Depth.Value
                                
                                # Model thread
                                if hasattr(subobj, "ModelThread"):
                                    model_thread = bool(subobj.ModelThread)
                                
                                # Check if there are thread features
                                has_thread = ('Threaded' in subobj.TypeId or 
                                             (hasattr(subobj, "Threaded") and subobj.Threaded) or
                                             model_thread)
                                
                                # Check if the hole is in the center of the disk
                                if hasattr(subobj, "Placement"):
                                    hole_center = subobj.Placement.Base
                                    # Calculate distance between hole center and disk center
                                    distance = math.sqrt((hole_center.x - disk_center.x)**2 + 
                                                         (hole_center.y - disk_center.y)**2)
                                    # If distance is less than 10% of hole radius, consider it centered
                                    is_centered = distance < hole_radius * 0.1
                
                # Objects created directly using the Part module
                elif obj.TypeId == 'Part::Cylinder':
                    if not cylinder_found:  # Only process the first disk found
                        cylinder_found = True
                        if hasattr(obj, "Radius"):
                            disk_radius = obj.Radius.Value
                        elif hasattr(obj.Shape, "Radius"):
                            disk_radius = obj.Shape.Radius
                        
                        if hasattr(obj, "Height"):
                            disk_height = obj.Height.Value
                        elif hasattr(obj.Shape, "Height"):
                            disk_height = obj.Shape.Height
                        
                        # Record disk center position
                        if hasattr(obj, "Placement"):
                            disk_center = obj.Placement.Base
    
    # If no explicit thread features are found, try to detect through geometric analysis
    if not has_thread and hole_found:
        for obj in doc.Objects:
            if hasattr(obj, "Shape") and hasattr(obj.Shape, "Edges"):
                # Check for helical edges, which might indicate threads
                for edge in obj.Shape.Edges:
                    if hasattr(edge, "Curve") and hasattr(edge.Curve, "isHelical") and edge.Curve.isHelical():
                        has_thread = True
                        # Try to get pitch from helix
                        if hasattr(edge.Curve, "Pitch"):
                            thread_pitch = edge.Curve.Pitch
                        break
    
    # Return result
    def extract_value(val):
        if val is None:
            return None
        try:
            if isinstance(val, str) and ' ' in val:
                return float(val.split()[0])
            return float(val)
        except:
            return val
    
    result = {
        'disk_radius': extract_value(disk_radius),
        'disk_height': extract_value(disk_height),
        'thread_size': thread_size,
        'thread_depth': extract_value(thread_depth),
        'model_thread': model_thread,
        'has_thread': has_thread,
        'is_centered': is_centered
    }
                    `
                    sendEvent(FUNCTION_KEY_WORD_DETECTED, {
                        message: `Detected ${FUNCTION_BEHAVIOR} operation`,
                        filename: this.filename,
                        code: pythonCode
                    });
                }
                // Detect keywords
              } catch (error) {
                  sendEvent(ERROR, {
                      error_type: "general_error",
                      message: `Execution error: ${error.message}`
                  });
              }
          }
      });
      
      sendEvent(HOOK_INSTALLED, {
          message: `Hook installation completed, waiting for ${FUNCTION_BEHAVIOR} operation...`
      });
  }
  
  // Execute hook initialization immediately
  initHook();
})();


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

"""
FreeCAD Disk with Threaded Hole Event Handler
Responsible for processing events generated by the hook script and updating evaluation metrics
"""

from typing import Dict, Any, Optional, List

# 事件类型常量
SCRIPT_INITIALIZED = "script_initialized"
FUNCTION_NOT_FOUND = "function_not_found"
FUNCTION_FOUND = "function_found"
FUNCTION_CALLED = "function_called"
FUNCTION_KEY_WORD_DETECTED = "function_key_word_detected"
ERROR = "error"
HOOK_INSTALLED = "hook_installed"

# Keyword-related constants
DISK_RADIUS = "disk_radius"
DISK_HEIGHT = "disk_height"
THREAD_SIZE = "thread_size"
THREAD_DEPTH = "thread_depth"
MODEL_THREAD = "model_thread"
HAS_THREAD = "has_thread"
IS_CENTERED = "is_centered"

def execute_python_code(code: str, logger: Any) -> Dict[str, Any]:
    """
    Execute Python code and return the result
    
    Args:
        code: Python code to execute
        logger: Logger object
        
    Returns:
        Dict[str, Any]: Execution result
    """
    try:
        # Create a new namespace to execute the code
        namespace = {}
        exec(code, namespace)
        result = namespace.get('result', None)
        
        if result is None:
            logger.warning("Disk with threaded hole object not found")
            return None
            
        # Validate result format
        required_keys = [
            DISK_RADIUS, DISK_HEIGHT, THREAD_SIZE, THREAD_DEPTH, 
            MODEL_THREAD, HAS_THREAD, IS_CENTERED
        ]
        if not all(key in result for key in required_keys):
            logger.error(f"Result missing required keys: {required_keys}")
            return None
            
        return result
    except Exception as e:
        logger.error(f"Error executing Python code: {str(e)}")
        return None

def message_handler(message: Dict[str, Any], logger: Any, task_parameter: Dict[str, Any]) -> Optional[List[Dict[str, Any]]]:
    """
    Process messages received from the hook script
    
    Args:
        message: Frida message object
        logger: Logger object
        task_parameter: Task parameters
        
    Returns:
        Optional[List[Dict[str, Any]]]: List of status updates, or None if no updates
    """
    updates = []
    
    if message.get('type') == 'send' and 'payload' in message:
        payload = message['payload']
        
        if 'event' in payload:
            event_type = payload['event']
            logger.debug(f"Event received: {event_type}")
            
            if event_type == SCRIPT_INITIALIZED:
                logger.info(f"Hook script initialized: {payload.get('message', '')}")
                
            elif event_type == FUNCTION_FOUND:
                logger.info(f"Function found: {payload.get('address', '')}")
                
            elif event_type == FUNCTION_CALLED:
                logger.info(f"Function called: {payload.get('message', '')}")
                # Update first key step status
                updates.append({
                    'status': 'key_step',
                    'index': 1,
                    'name': 'Document saved'
                })
                
            elif event_type == FUNCTION_KEY_WORD_DETECTED:
                # 执行Python代码并获取结果
                code = payload.get('code', '')
                filename = payload.get('filename', '')
                expected_path = task_parameter.get("source_path", "") + task_parameter.get("filename", "")
                logger.info(f"Detected keywords, document path: {filename}, expected document path: {expected_path}")
                
                if filename == expected_path:
                    result = execute_python_code(code, logger)
                    if result:
                        # Check if disk with threaded hole parameters meet expectations
                        expected_disk_radius = task_parameter.get(DISK_RADIUS, 50.0)
                        expected_disk_height = task_parameter.get(DISK_HEIGHT, 10.0)
                        expected_thread_size = task_parameter.get(THREAD_SIZE, "M10")
                        expected_thread_depth = task_parameter.get(THREAD_DEPTH, 10.0)
                        expected_model_thread = task_parameter.get(MODEL_THREAD, True)

                        actual_disk_radius = result[DISK_RADIUS]
                        actual_disk_height = result[DISK_HEIGHT]
                        actual_thread_size = result[THREAD_SIZE]
                        actual_thread_depth = result[THREAD_DEPTH]
                        actual_model_thread = result[MODEL_THREAD]
                        actual_has_thread = result[HAS_THREAD]
                        actual_is_centered = result[IS_CENTERED]

                        # Log key parameters
                        logger.info(f"Task parameter check:")
                        logger.info(f"Expected disk: radius={expected_disk_radius}, height={expected_disk_height}")
                        logger.info(f"Expected threaded hole: Size={expected_thread_size}, depth={expected_thread_depth}")
                        logger.info(f"Expected model thread: {expected_model_thread}")
                        
                        logger.info(f"Actual disk: radius={actual_disk_radius}, height={actual_disk_height}")
                        logger.info(f"Actual threaded hole: Size={actual_thread_size}, depth={actual_thread_depth}")
                        logger.info(f"Actual model thread: {actual_model_thread}")
                        logger.info(f"Has thread: {actual_has_thread}, Is centered: {actual_is_centered}")

                        try:
                            # Try to get numeric part (handling possible units)
                            def get_float_value(value):
                                if isinstance(value, str) and ' ' in value:
                                    return float(value.split()[0])
                                return float(value)
                            
                            actual_disk_radius_value = get_float_value(actual_disk_radius)
                            actual_disk_height_value = get_float_value(actual_disk_height)
                            actual_thread_depth_value = get_float_value(actual_thread_depth)
                            
                            # Allow a certain margin of error (0.01%)
                            disk_radius_error = abs((actual_disk_radius_value - expected_disk_radius) / expected_disk_radius) <= 0.0001
                            disk_height_error = abs((actual_disk_height_value - expected_disk_height) / expected_disk_height) <= 0.0001
                            thread_depth_error = abs((actual_thread_depth_value - expected_thread_depth) / expected_thread_depth) <= 0.0001
                            
                            # Check if thread parameters match
                            def compare_str(actual, expected):
                                return actual.strip().lower() == expected.strip().lower()
                                
                            thread_size_correct = compare_str(actual_thread_size, expected_thread_size)
                            model_thread_correct = actual_model_thread == expected_model_thread
                            
                            is_centered_correct = actual_is_centered
                            has_thread_correct = actual_has_thread
                        except Exception as e:
                            logger.error(f"Parameter comparison error: {str(e)}")
                            disk_radius_error = False
                            disk_height_error = False
                            thread_size_correct = False
                            thread_depth_error = False
                            model_thread_correct = False
                            is_centered_correct = False
                            has_thread_correct = False
                        
                        disk_correct = disk_radius_error and disk_height_error
                        thread_correct = (thread_size_correct and thread_depth_error and 
                                         model_thread_correct and has_thread_correct)
                        hole_correct = is_centered_correct
                        
                        if disk_correct and hole_correct and thread_correct:
                            # Update second key step status
                            updates.append({
                                'status': 'key_step',
                                'index': 2,
                                'name': 'Created disk with threaded hole and saved successfully'
                            })
                            
                            # Task completed successfully
                            updates.append({
                                'status': 'success',
                                'reason': 'Successfully created a disk with threaded hole according to requirements and saved it'
                            })
                        else:
                            logger.warning(f"Parameter validation failed: " + 
                                          f"Disk correct: {disk_correct}, " +
                                          f"Hole position correct: {hole_correct}, " +
                                          f"Thread correct: {thread_correct}")
                
            elif event_type == ERROR:
                error_type = payload.get("error_type", "unknown")
                error_message = payload.get("message", "Unknown error")
                
                logger.error(f"Hook script error ({error_type}): {error_message}")
                
                # Record error event
                updates.append({
                    'status': 'error',
                    'type': error_type,
                    'message': error_message
                })
                
    elif message.get('type') == 'error':
        logger.error(f"Hook script error: {message.get('stack', '')}")
        
        # Record error event
        updates.append({
            'status': 'error',
            'type': 'script_error',
            'message': message.get('stack', 'Unknown error')
        })
    
    return updates if updates else None