diff --git a/copilot_agent_client/mcp_agent_loop.py b/copilot_agent_client/mcp_agent_loop.py index d9f93b4..d40af6c 100644 --- a/copilot_agent_client/mcp_agent_loop.py +++ b/copilot_agent_client/mcp_agent_loop.py @@ -5,13 +5,14 @@ sys.path.append(".") import json +import subprocess from PIL import Image import io from tools.image_tools import make_b64_url -from copilot_front_end.mobile_action_helper import capture_screenshot, dectect_screen_on, press_home_key +from copilot_front_end.mobile_action_helper import capture_screenshot, dectect_screen_on, press_home_key, _get_adb_command from copilot_front_end.mobile_action_helper import init_device, open_screen from copilot_front_end.pu_frontend_executor import act_on_device, uiTars_to_frontend_action @@ -27,6 +28,26 @@ import threading + +def _check_yadb_installed(device_id, print_command=False): + """ + 只检查 yadb 是否安装,不按 HOME 键。 + """ + adb_command = _get_adb_command(device_id) + + command = f"{adb_command} shell md5sum /data/local/tmp/yadb" + if print_command: + print(f"Executing command: {command}") + + result = subprocess.run(command, shell=True, capture_output=True, text=True) + if "29a0cd3b3adea92350dd5a25594593df" not in result.stdout: + command = f"{adb_command} push yadb /data/local/tmp" + print(f"YADB is not installed on the device. Installing now...") + if print_command: + print(f"Executing command: {command}") + subprocess.run(command, shell=True, capture_output=True, text=True) + + def auto_reply(current_image_url, task, info_action, model_provider, model_name): """ Reply with information action. @@ -193,11 +214,15 @@ def gui_agent_loop( # init device for the first time open_screen(device_id) - init_device(device_id) - - # if reset_environment, press home key before starting the task + + # 只在需要重置环境时调用 init_device(它内部会按 HOME 键) + # 否则只检查 yadb 是否安装,不按 HOME 键 if reset_environment and session_id is None and task is not None: - press_home_key(device_id, print_command=True) + init_device(device_id, reset_environment=True) # init_device 内部会按 HOME 键 + else: + # 只检查 yadb,不按 HOME 键 + init_device(device_id, reset_environment=False) + # task, task_type = task, rollout_config['task_type'] task_type = agent_loop_config['task_type'] @@ -265,6 +290,9 @@ def gui_agent_loop( global_step_idx = 0 # restart the steps from 0, even continuing an existing session for step_idx in range(max_steps): + # 打印步骤开始分隔符 + step_label = f" Step {step_idx+1} start " + print(f"\n{step_label:-^50}") if not dectect_screen_on(device_id): print("Screen is off, turn on the screen first") @@ -371,7 +399,17 @@ def gui_agent_loop( history_actions.append(action) - print(f"Step {step_idx+1}/{max_steps} done.\nAction Type: {action['action_type']}, cot: {action.get('cot', '')}\nSession ID: {session_id}\n") + # 清理 cot 中的 标签用于打印 + cot_display = action.get('cot', '') + if cot_display: + import re + cot_display = re.sub(r'<\s*/?THINK\s*>', '', cot_display, flags=re.IGNORECASE).strip() + + print(f"Action: {action['action_type']}") + if cot_display: + print(f"cot: {cot_display}") + step_end_label = f" Step {step_idx+1} end " + print(f"{step_end_label:-^50}") # print(f"local:{step_idx+1}/global:{global_step_idx}/{max_steps} done. Action: {action}") @@ -446,5 +484,3 @@ def gui_agent_loop( # print(f"Task {task} done in {len(history_actions)} steps. Session ID: {session_id}") return return_log - - diff --git a/copilot_agent_client/pu_client.py b/copilot_agent_client/pu_client.py index b4c4ec0..a97cbba 100644 --- a/copilot_agent_client/pu_client.py +++ b/copilot_agent_client/pu_client.py @@ -5,23 +5,19 @@ sys.path.append(".") import json - from PIL import Image import io +from collections import OrderedDict from tools.image_tools import draw_points, make_b64_url - -from copilot_front_end.mobile_action_helper import capture_screenshot, dectect_screen_on, press_home_key - +from copilot_front_end.mobile_action_helper import capture_screenshot, dectect_screen_on from copilot_front_end.mobile_action_helper import init_device, open_screen from copilot_front_end.pu_frontend_executor import act_on_device, uiTars_to_frontend_action - from megfile import smart_remove - import time - from tools.ask_llm_v2 import ask_llm_anything + def reply_info_action(current_image_url, task, info_action, model_provider, model_name): """ Reply with information action. @@ -32,22 +28,18 @@ def reply_info_action(current_image_url, task, info_action, model_provider, mode "content": [ { "type": "text", - "text": f"""# 角色 + "text": f"""# 角色 你将扮演一个正在使用GUI Agent完成任务的用户。 - # 任务 阅读下方提供的所有背景信息,针对[Agent的澄清问题],生成一个提供关键信息的、简短直接的回答。 - # 背景信息 - **任务目标:** {task} - **agent 问的问题:** {json.dumps(info_action, ensure_ascii=False)} - # 输出要求 - 你的回答必须极其简短和明确。 - 你的回答应直接命中问题的核心,解决Agent的疑惑。 - 不要进行任何额外的解释、对话或使用礼貌用语。 - 只输出回答本身,不要添加任何引号或其他修饰。 - 以下是当前页面内容: """, }, @@ -64,7 +56,6 @@ def reply_info_action(current_image_url, task, info_action, model_provider, mode ] } ] - response = ask_llm_anything( model_provider=model_provider, model_name=model_name, @@ -76,43 +67,55 @@ def reply_info_action(current_image_url, task, info_action, model_provider, mode "frequency_penalty": 0.0, } ) - if "" in response: response = response.split("")[-1].strip() - return response -# delay after act on device -# rollout config -# device info -# def evaluate_task_on_device(agent_server, device_info, task, frontend_action_converter, ask_action_function_func, max_steps = 40, delay_after_capture = 2): -def evaluate_task_on_device(agent_server, device_info, task, rollout_config, extra_info = {}, reflush_app=True, auto_reply = False, reset_environment=True): + +def evaluate_task_on_device(agent_server, device_info, task, rollout_config, extra_info={}, reflush_app=True, auto_reply=False, reset_environment=False): """ Evaluate a task on a device using the provided frontend action converter and action function. - """ + # ===== 新增:本地美化函数 ===== + def _pretty_format_action(act): + if not isinstance(act, (dict, OrderedDict)): + return str(act) + lines = [] + # 不再打印 标签 + if 'cot' in act and act['cot']: + cot_clean = str(act['cot']).replace('\n', ' ').replace('\r', ' ') + # 移除 标签 + import re + cot_clean = re.sub(r'<\s*/?THINK\s*>', '', cot_clean, flags=re.IGNORECASE).strip() + if cot_clean: + lines.append(f"cot: {cot_clean}") + # Define field order for readability + field_order = ['explain', 'action', 'value', 'point', 'point1', 'point2', 'return', 'summary'] + for key in field_order: + if key in act: + val = act[key] + if isinstance(val, list): + val_str = ",".join(str(x) for x in val) + else: + val_str = str(val).replace('\n', ' ').strip() + lines.append(f"{key}: {val_str}") + return "\n".join(lines) + # ============================ + # init device for the first time device_id = device_info['device_id'] open_screen(device_id) - init_device(device_id) - - - if reset_environment: - press_home_key(device_id, print_command=True) + init_device(device_id, reset_environment=reset_environment) task, task_type = task, rollout_config['task_type'] - session_id = agent_server.get_session({ "task": task, "task_type": task_type, "model_config": rollout_config['model_config'], "extra_info": extra_info - }) - print(f"Session ID: {session_id}") - return_log = { "session_id": session_id, "device_info": device_info, @@ -120,25 +123,24 @@ def evaluate_task_on_device(agent_server, device_info, task, rollout_config, ext "rollout_config": rollout_config, "extra_info": extra_info } - device_id, device_wm_size = device_info['device_id'], device_info['device_wm_size'] - max_steps = rollout_config.get('max_steps', 40) delay_after_capture = rollout_config.get('delay_after_capture', 2) - history_actions = [] for step_idx in range(max_steps): - + # 打印步骤开始分隔符 + step_label = f" Step {step_idx+1} start " + print(f"\n{step_label:-^50}") + if not dectect_screen_on(device_id): print("Screen is off, turn on the screen first") break image_path = capture_screenshot(device_id, "tmp_screenshot", print_command=False) - image_b64_url = make_b64_url(image_path, resize_config=rollout_config['model_config'].get("resize_config", None)) smart_remove(image_path) - + payload = { "session_id": session_id, "observation": { @@ -150,43 +152,42 @@ def evaluate_task_on_device(agent_server, device_info, task, rollout_config, ext }, } } - if history_actions[-1]['action_type'] == "INFO" if len(history_actions) > 0 else False: - info_action = history_actions[-1] + if history_actions and history_actions[-1]['action_type'] == "INFO": + info_action = history_actions[-1] if auto_reply: print(f"AUTO REPLY INFO FROM MODEL!") - reply_info = reply_info_action(image_b64_url, task, info_action, model_provider=rollout_config['model_config']['model_provider'], model_name=rollout_config['model_config']['model_name']) + reply_info = reply_info_action( + image_b64_url, task, info_action, + model_provider=rollout_config['model_config']['model_provider'], + model_name=rollout_config['model_config']['model_name'] + ) print(f"info: {reply_info}") - else: print(f"EN: Agent asks: {history_actions[-1]['value']} Please Reply: ") print(f"ZH: Agent 问你: {history_actions[-1]['value']} 回复一下:") - reply_info = input("Your reply:") - print(f"Replied info action: {reply_info}") - payload['observation']['query'] = reply_info - action = agent_server.automate_step(payload)['action'] - - #TODO: to replace with the new function action = uiTars_to_frontend_action(action) - - act_on_device(action, device_id, device_wm_size, print_command=True, reflush_app=reflush_app) - + act_on_device(action, device_id, device_wm_size, print_command=True, reflush_app=reflush_app, print_executing_command=True) history_actions.append(action) - - print(f"Step {step_idx+1}/{max_steps} done. Action: {action}") + # ===== 替换原始打印:使用美观格式 ===== + print(f"Action: {action['action_type']}") + print(_pretty_format_action(action)) + step_end_label = f" Step {step_idx+1} end " + print(f"{step_end_label:-^50}") + # =================================== if action['action_type'].upper() in ['COMPLETE', "ABORT"]: stop_reason = action['action_type'].upper() break time.sleep(delay_after_capture) - + if action['action_type'] in ['COMPLETE', "ABORT"]: stop_reason = action['action_type'] elif step_idx == max_steps - 1: @@ -194,13 +195,7 @@ def evaluate_task_on_device(agent_server, device_info, task, rollout_config, ext else: stop_reason = "MANUAL_STOP" - # return_log['session_id'] = session_id return_log['stop_reason'] = stop_reason - return_log['stop_steps'] = step_idx + 1 - - print(f"Task {task} done in {len(history_actions)} steps. Session ID: {session_id}") - + print(f"\ndone in {len(history_actions)} steps.\nSession ID: {session_id}") return return_log - - diff --git a/copilot_front_end/mobile_action_helper.py b/copilot_front_end/mobile_action_helper.py index a8efae6..584b835 100644 --- a/copilot_front_end/mobile_action_helper.py +++ b/copilot_front_end/mobile_action_helper.py @@ -75,7 +75,7 @@ def press_home_key(device_id, print_command = False): subprocess.run(command, shell=True, capture_output=True, text=True) -def init_device(device_id, print_command = False): +def init_device(device_id, reset_environment=False, print_command = False): """ Initialize the device by checking if yadb is installed. """ @@ -99,9 +99,12 @@ def init_device(device_id, print_command = False): subprocess.run(command, shell=True, capture_output=True, text=True) else: - print("yadb is already installed on the device.") + # print("yadb is already installed on the device.") + pass + + if reset_environment: + press_home_key(device_id, print_command=print_command) - # press_home_key(device_id, print_command=print_command) def init_all_devices(): """ @@ -109,9 +112,10 @@ def init_all_devices(): """ devices = list_devices() for device_id in tqdm(devices): - init_device(device_id) + init_device(device_id, reset_environment=True) print(f"Initialized device: {device_id}") + def dectect_screen_on(device_id, print_command = False): """ Detect whether the screen is on for the specified device. @@ -636,9 +640,10 @@ def __init__(self, device_id = None): self.device_id = device_id self.wm_size = get_device_wm_size(self.device_id) if self.device_id is not None: - init_device(self.device_id, print_command=True) + init_device(self.device_id, reset_environment=True, print_command=True) # _open_screen(self.device_id, print_command=True) + pass def set_device_id(self, device_id): @@ -730,4 +735,4 @@ def step_interaction(self, action, capture_duration = 0.5, image_full_path = Non print(get_device_wm_size("bc23727a")) open_screen(None, print_command=True) - pass \ No newline at end of file + pass diff --git a/copilot_front_end/package_map.py b/copilot_front_end/package_map.py index 99401aa..d648cc8 100644 --- a/copilot_front_end/package_map.py +++ b/copilot_front_end/package_map.py @@ -209,6 +209,7 @@ "osmAnd": "net.osmand", "给到": "com.guanaitong", "百词斩": "com.jiongji.andriod.card", + "象棋": "com.tencent.qqgame.xq", } diff --git a/copilot_tools/scrcpy/linux/icon.png b/copilot_tools/scrcpy/linux/icon.png new file mode 100644 index 0000000..b96a1af Binary files /dev/null and b/copilot_tools/scrcpy/linux/icon.png differ diff --git a/copilot_tools/scrcpy/linux/scrcpy b/copilot_tools/scrcpy/linux/scrcpy new file mode 100644 index 0000000..48c4b75 Binary files /dev/null and b/copilot_tools/scrcpy/linux/scrcpy differ diff --git a/copilot_tools/scrcpy/linux/scrcpy-server b/copilot_tools/scrcpy/linux/scrcpy-server new file mode 100644 index 0000000..b36f14d Binary files /dev/null and b/copilot_tools/scrcpy/linux/scrcpy-server differ diff --git a/copilot_tools/scrcpy/linux/scrcpy.1 b/copilot_tools/scrcpy/linux/scrcpy.1 new file mode 100644 index 0000000..d72fda1 --- /dev/null +++ b/copilot_tools/scrcpy/linux/scrcpy.1 @@ -0,0 +1,860 @@ +.TH "scrcpy" "1" +.SH NAME +scrcpy \- Display and control your Android device + + +.SH SYNOPSIS +.B scrcpy +.RI [ options ] + + +.SH DESCRIPTION +.B scrcpy +provides display and control of Android devices connected on USB (or over TCP/IP). It does not require any root access. + + +.SH OPTIONS + +.TP +.B \-\-always\-on\-top +Make scrcpy window always on top (above other windows). + +.TP +.BI "\-\-angle " degrees +Rotate the video content by a custom angle, in degrees (clockwise). + +.TP +.BI "\-\-audio\-bit\-rate " value +Encode the audio at the given bit rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000). + +Default is 128K (128000). + +.TP +.BI "\-\-audio\-buffer " ms +Configure the audio buffering delay (in milliseconds). + +Lower values decrease the latency, but increase the likelihood of buffer underrun (causing audio glitches). + +Default is 50. + +.TP +.BI "\-\-audio\-codec " name +Select an audio codec (opus, aac, flac or raw). + +Default is opus. + +.TP +.BI "\-\-audio\-codec\-options " key\fR[:\fItype\fR]=\fIvalue\fR[,...] +Set a list of comma-separated key:type=value options for the device audio encoder. + +The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'. + +The list of possible codec options is available in the Android documentation: + + + +.TP +.B \-\-audio\-dup +Duplicate audio (capture and keep playing on the device). + +This feature is only available with --audio-source=playback. + +.TP +.BI "\-\-audio\-encoder " name +Use a specific MediaCodec audio encoder (depending on the codec provided by \fB\-\-audio\-codec\fR). + +The available encoders can be listed by \fB\-\-list\-encoders\fR. + +.TP +.BI "\-\-audio\-source " source +Select the audio source. Possible values are: + + - "output": forwards the whole audio output, and disables playback on the device. + - "playback": captures the audio playback (Android apps can opt-out, so the whole output is not necessarily captured). + - "mic": captures the microphone. + - "mic-unprocessed": captures the microphone unprocessed (raw) sound. + - "mic-camcorder": captures the microphone tuned for video recording, with the same orientation as the camera if available. + - "mic-voice-recognition": captures the microphone tuned for voice recognition. + - "mic-voice-communication": captures the microphone tuned for voice communications (it will for instance take advantage of echo cancellation or automatic gain control if available). + - "voice-call": captures voice call. + - "voice-call-uplink": captures voice call uplink only. + - "voice-call-downlink": captures voice call downlink only. + - "voice-performance": captures audio meant to be processed for live performance (karaoke), includes both the microphone and the device playback. + +Default is output. + +.TP +.BI "\-\-audio\-output\-buffer " ms +Configure the size of the SDL audio output buffer (in milliseconds). + +If you get "robotic" audio playback, you should test with a higher value (10). Do not change this setting otherwise. + +Default is 5. + +.TP +.BI "\-b, \-\-video\-bit\-rate " value +Encode the video at the given bit rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000). + +Default is 8M (8000000). + +.TP +.BI "\-\-camera\-ar " ar +Select the camera size by its aspect ratio (+/- 10%). + +Possible values are "sensor" (use the camera sensor aspect ratio), "\fInum\fR:\fIden\fR" (e.g. "4:3") and "\fIvalue\fR" (e.g. "1.6"). + +.TP +.BI "\-\-camera\-facing " facing +Select the device camera by its facing direction. + +Possible values are "front", "back" and "external". + +.TP +.BI "\-\-camera\-fps " fps +Specify the camera capture frame rate. + +If not specified, Android's default frame rate (30 fps) is used. + +.TP +.B \-\-camera\-high\-speed +Enable high-speed camera capture mode. + +This mode is restricted to specific resolutions and frame rates, listed by \fB\-\-list\-camera\-sizes\fR. + +.TP +.BI "\-\-camera\-id " id +Specify the device camera id to mirror. + +The available camera ids can be listed by \fB\-\-list\-cameras\fR. + +.TP +.BI "\-\-camera\-size " width\fRx\fIheight +Specify an explicit camera capture size. + +.TP +.BI "\-\-capture\-orientation " value +Possible values are 0, 90, 180, 270, flip0, flip90, flip180 and flip270, possibly prefixed by '@'. + +The number represents the clockwise rotation in degrees; the "flip" keyword applies a horizontal flip before the rotation. + +If a leading '@' is passed (@90) for display capture, then the rotation is locked, and is relative to the natural device orientation. + +If '@' is passed alone, then the rotation is locked to the initial device orientation. + +Default is 0. + +.TP +.BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy +Crop the device screen on the server. + +The values are expressed in the device natural orientation (typically, portrait for a phone, landscape for a tablet). + +.TP +.B \-d, \-\-select\-usb +Use USB device (if there is exactly one, like adb -d). + +Also see \fB\-e\fR (\fB\-\-select\-tcpip\fR). + +.TP +.BI "\-\-disable\-screensaver" +Disable screensaver while scrcpy is running. + +.TP +.BI "\-\-display\-id " id +Specify the device display id to mirror. + +The available display ids can be listed by \fB\-\-list\-displays\fR. + +Default is 0. + +.TP +.BI "\-\-display\-ime\-policy " value +Set the policy for selecting where the IME should be displayed. + +Possible values are "local", "fallback" and "hide": + + - "local" means that the IME should appear on the local display. + - "fallback" means that the IME should appear on a fallback display (the default display). + - "hide" means that the IME should be hidden. + +By default, the IME policy is left unchanged. + + +.TP +.BI "\-\-display\-orientation " value +Set the initial display orientation. + +Possible values are 0, 90, 180, 270, flip0, flip90, flip180 and flip270. The number represents the clockwise rotation in degrees; the "flip" keyword applies a horizontal flip before the rotation. + +Default is 0. + +.TP +.B \-e, \-\-select\-tcpip +Use TCP/IP device (if there is exactly one, like adb -e). + +Also see \fB\-d\fR (\fB\-\-select\-usb\fR). + +.TP +.B \-f, \-\-fullscreen +Start in fullscreen. + +.TP +.B \-\-force\-adb\-forward +Do not attempt to use "adb reverse" to connect to the device. + +.TP +.B \-G +Same as \fB\-\-gamepad=uhid\fR, or \fB\-\-keyboard=aoa\fR if \fB\-\-otg\fR is set. + +.TP +.BI "\-\-gamepad " mode +Select how to send gamepad inputs to the device. + +Possible values are "disabled", "uhid" and "aoa": + + - "disabled" does not send gamepad inputs to the device. + - "uhid" simulates physical HID gamepads using the Linux HID kernel module on the device. + - "aoa" simulates physical HID gamepads using the AOAv2 protocol. It may only work over USB. + +Also see \fB\-\-keyboard\f and R\fB\-\-mouse\fR. +.TP +.B \-h, \-\-help +Print this help. + +.TP +.B \-K +Same as \fB\-\-keyboard=uhid\fR, or \fB\-\-keyboard=aoa\fR if \fB\-\-otg\fR is set. + +.TP +.BI "\-\-keyboard " mode +Select how to send keyboard inputs to the device. + +Possible values are "disabled", "sdk", "uhid" and "aoa": + + - "disabled" does not send keyboard inputs to the device. + - "sdk" uses the Android system API to deliver keyboard events to applications. + - "uhid" simulates a physical HID keyboard using the Linux HID kernel module on the device. + - "aoa" simulates a physical HID keyboard using the AOAv2 protocol. It may only work over USB. + +For "uhid" and "aoa", the keyboard layout must be configured (once and for all) on the device, via Settings -> System -> Languages and input -> Physical keyboard. This settings page can be started directly using the shortcut MOD+k (except in OTG mode), or by executing: + + adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS + +This option is only available when the HID keyboard is enabled (or a physical keyboard is connected). + +Also see \fB\-\-mouse\fR and \fB\-\-gamepad\fR. + +.TP +.B \-\-kill\-adb\-on\-close +Kill adb when scrcpy terminates. + +.TP +.B \-\-legacy\-paste +Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+Shift+v). + +This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically. + +.TP +.B \-\-list\-apps +List Android apps installed on the device. + +.TP +.B \-\-list\-camera\-sizes +List the valid camera capture sizes. + +.TP +.B \-\-list\-cameras +List cameras available on the device. + +.TP +.B \-\-list\-encoders +List video and audio encoders available on the device. + +.TP +.B \-\-list\-displays +List displays available on the device. + +.TP +.BI "\-m, \-\-max\-size " value +Limit both the width and height of the video to \fIvalue\fR. The other dimension is computed so that the device aspect\-ratio is preserved. + +Default is 0 (unlimited). + +.TP +.B \-M +Same as \fB\-\-mouse=uhid\fR, or \fB\-\-mouse=aoa\fR if \fB\-\-otg\fR is set. + +.TP +.BI "\-\-max\-fps " value +Limit the framerate of screen capture (officially supported since Android 10, but may work on earlier versions). + +.TP +.BI "\-\-mouse " mode +Select how to send mouse inputs to the device. + +Possible values are "disabled", "sdk", "uhid" and "aoa": + + - "disabled" does not send mouse inputs to the device. + - "sdk" uses the Android system API to deliver mouse events to applications. + - "uhid" simulates a physical HID mouse using the Linux HID kernel module on the device. + - "aoa" simulates a physical mouse using the AOAv2 protocol. It may only work over USB. + +In "uhid" and "aoa" modes, the computer mouse is captured to control the device directly (relative mouse mode). + +LAlt, LSuper or RSuper toggle the capture mode, to give control of the mouse back to the computer. + +Also see \fB\-\-keyboard\fR and \fB\-\-gamepad\fR. + +.TP +.BI "\-\-mouse\-bind " xxxx[:xxxx] +Configure bindings of secondary clicks. + +The argument must be one or two sequences (separated by ':') of exactly 4 characters, one for each secondary click (in order: right click, middle click, 4th click, 5th click). + +The first sequence defines the primary bindings, used when a mouse button is pressed alone. The second sequence defines the secondary bindings, used when a mouse button is pressed while the Shift key is held. + +If the second sequence of bindings is omitted, then it is the same as the first one. + +Each character must be one of the following: + + - '+': forward the click to the device + - '-': ignore the click + - 'b': trigger shortcut BACK (or turn screen on if off) + - 'h': trigger shortcut HOME + - 's': trigger shortcut APP_SWITCH + - 'n': trigger shortcut "expand notification panel" + +Default is 'bhsn:++++' for SDK mouse, and '++++:bhsn' for AOA and UHID. + + +.TP +.B \-n, \-\-no\-control +Disable device control (mirror the device in read\-only). + +.TP +.B \-N, \-\-no\-playback +Disable video and audio playback on the computer (equivalent to \fB\-\-no\-video\-playback \-\-no\-audio\-playback\fR). + +.TP +\fB\-\-new\-display\fR[=[\fIwidth\fRx\fIheight\fR][/\fIdpi\fR]] +Create a new display with the specified resolution and density. If not provided, they default to the main display dimensions and DPI. + +Examples: + + \-\-new\-display=1920x1080 + \-\-new\-display=1920x1080/420 + \-\-new\-display # main display size and density + \-\-new\-display=/240 # main display size and 240 dpi + +.TP +.B \-\-no\-audio +Disable audio forwarding. + +.TP +.B \-\-no\-audio\-playback +Disable audio playback on the computer. + +.TP +.B \-\-no\-cleanup +By default, scrcpy removes the server binary from the device and restores the device state (show touches, stay awake and power mode) on exit. + +This option disables this cleanup. + +.TP +.B \-\-no\-clipboard\-autosync +By default, scrcpy automatically synchronizes the computer clipboard to the device clipboard before injecting Ctrl+v, and the device clipboard to the computer clipboard whenever it changes. + +This option disables this automatic synchronization. + +.TP +.B \-\-no\-downsize\-on\-error +By default, on MediaCodec error, scrcpy automatically tries again with a lower definition. + +This option disables this behavior. + +.TP +.B \-\-no\-key\-repeat +Do not forward repeated key events when a key is held down. + +.TP +.B \-\-no\-mipmaps +If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then mipmaps are automatically generated to improve downscaling quality. This option disables the generation of mipmaps. + +.TP +.B \-\-no\-mouse\-hover +Do not forward mouse hover (mouse motion without any clicks) events. + +.TP +.B \-\-no\-power\-on +Do not power on the device on start. + +.TP +.B \-\-no\-vd\-destroy\-content +Disable virtual display "destroy content on removal" flag. + +With this option, when the virtual display is closed, the running apps are moved to the main display rather than being destroyed. + +.TP +.B \-\-no\-vd\-system\-decorations +Disable virtual display system decorations flag. + +.TP +.B \-\-no\-video +Disable video forwarding. + +.TP +.B \-\-no\-video\-playback +Disable video playback on the computer. + +.TP +.B \-\-no\-window +Disable scrcpy window. Implies --no-video-playback. + +.TP +.BI "\-\-orientation " value +Same as --display-orientation=value --record-orientation=value. + +.TP +.B \-\-otg +Run in OTG mode: simulate physical keyboard and mouse, as if the computer keyboard and mouse were plugged directly to the device via an OTG cable. + +In this mode, adb (USB debugging) is not necessary, and mirroring is disabled. + +LAlt, LSuper or RSuper toggle the mouse capture mode, to give control of the mouse back to the computer. + +If any of \fB\-\-hid\-keyboard\fR or \fB\-\-hid\-mouse\fR is set, only enable keyboard or mouse respectively, otherwise enable both. + +It may only work over USB. + +See \fB\-\-keyboard\fR, \fB\-\-mouse\fR and \fB\-\-gamepad\fR. + +.TP +.BI "\-p, \-\-port " port\fR[:\fIport\fR] +Set the TCP port (range) used by the client to listen. + +Default is 27183:27199. + +.TP +\fB\-\-pause\-on\-exit\fR[=\fImode\fR] +Configure pause on exit. Possible values are "true" (always pause on exit), "false" (never pause on exit) and "if-error" (pause only if an error occurred). + +This is useful to prevent the terminal window from automatically closing, so that error messages can be read. + +Default is "false". + +Passing the option without argument is equivalent to passing "true". + +.TP +.B \-\-power\-off\-on\-close +Turn the device screen off when closing scrcpy. + +.TP +.B \-\-prefer\-text +Inject alpha characters and space as text events instead of key events. + +This avoids issues when combining multiple keys to enter special characters, +but breaks the expected behavior of alpha keys in games (typically WASD). + +.TP +.B "\-\-print\-fps +Start FPS counter, to print framerate logs to the console. It can be started or stopped at any time with MOD+i. + +.TP +.BI "\-\-push\-target " path +Set the target directory for pushing files to the device by drag & drop. It is passed as\-is to "adb push". + +Default is "/sdcard/Download/". + +.TP +.BI "\-r, \-\-record " file +Record screen to +.IR file . + +The format is determined by the +.B \-\-record\-format +option if set, or by the file extension. + +.TP +.B \-\-raw\-key\-events +Inject key events for all input keys, and ignore text events. + +.TP +.BI "\-\-record\-format " format +Force recording format (mp4, mkv, m4a, mka, opus, aac, flac or wav). + +.TP +.BI "\-\-record\-orientation " value +Set the record orientation. + +Possible values are 0, 90, 180 and 270. The number represents the clockwise rotation in degrees. + +Default is 0. + +.TP +.BI "\-\-render\-driver " name +Request SDL to use the given render driver (this is just a hint). + +Supported names are currently "direct3d", "opengl", "opengles2", "opengles", "metal" and "software". + + + +.TP +.B \-\-require\-audio +By default, scrcpy mirrors only the video if audio capture fails on the device. This option makes scrcpy fail if audio is enabled but does not work. + +.TP +.BI "\-s, \-\-serial " number +The device serial number. Mandatory only if several devices are connected to adb. + +.TP +.B \-S, \-\-turn\-screen\-off +Turn the device screen off immediately. + +.TP +.B "\-\-screen\-off\-timeout " seconds +Set the screen off timeout while scrcpy is running (restore the initial value on exit). + +.TP +.BI "\-\-shortcut\-mod " key\fR[+...]][,...] +Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper". + +Several shortcut modifiers can be specified, separated by ','. + +For example, to use either LCtrl or LSuper for scrcpy shortcuts, pass "lctrl,lsuper". + +Default is "lalt,lsuper" (left-Alt or left-Super). + +.TP +.BI "\-\-start\-app " name +Start an Android app, by its exact package name. + +Add a '?' prefix to select an app whose name starts with the given name, case-insensitive (retrieving app names on the device may take some time): + + scrcpy --start-app=?firefox + +Add a '+' prefix to force-stop before starting the app: + + scrcpy --new-display --start-app=+org.mozilla.firefox + +Both prefixes can be used, in that order: + + scrcpy --start-app=+?firefox + +.TP +.B \-t, \-\-show\-touches +Enable "show touches" on start, restore the initial value on exit. + +It only shows physical touches (not clicks from scrcpy). + +.TP +.BI "\-\-tcpip\fR[=[+]\fIip\fR[:\fIport\fR]] +Configure and connect the device over TCP/IP. + +If a destination address is provided, then scrcpy connects to this address before starting. The device must listen on the given TCP port (default is 5555). + +If no destination address is provided, then scrcpy attempts to find the IP address and adb port of the current device (typically connected over USB), enables TCP/IP mode if necessary, then connects to this address before starting. + +Prefix the address with a '+' to force a reconnection. + +.TP +.BI "\-\-time\-limit " seconds +Set the maximum mirroring time, in seconds. + +.TP +.BI "\-\-tunnel\-host " ip +Set the IP address of the adb tunnel to reach the scrcpy server. This option automatically enables \fB\-\-force\-adb\-forward\fR. + +Default is localhost. + +.TP +.BI "\-\-tunnel\-port " port +Set the TCP port of the adb tunnel to reach the scrcpy server. This option automatically enables \fB\-\-force\-adb\-forward\fR. + +Default is 0 (not forced): the local port used for establishing the tunnel will be used. + +.TP +.B \-v, \-\-version +Print the version of scrcpy. + +.TP +.BI "\-V, \-\-verbosity " value +Set the log level ("verbose", "debug", "info", "warn" or "error"). + +Default is "info" for release builds, "debug" for debug builds. + +.TP +.BI "\-\-v4l2-sink " /dev/videoN +Output to v4l2loopback device. + +.TP +.BI "\-\-v4l2-buffer " ms +Add a buffering delay (in milliseconds) before pushing frames. This increases latency to compensate for jitter. + +This option is similar to \fB\-\-video\-buffer\fR, but specific to V4L2 sink. + +Default is 0 (no buffering). + +.TP +.BI "\-\-video\-buffer " ms +Add a buffering delay (in milliseconds) before displaying video frames. + +This increases latency to compensate for jitter. + +Default is 0 (no buffering). + +.TP +.BI "\-\-video\-codec " name +Select a video codec (h264, h265 or av1). + +Default is h264. + +.TP +.BI "\-\-video\-codec\-options " key\fR[:\fItype\fR]=\fIvalue\fR[,...] +Set a list of comma-separated key:type=value options for the device video encoder. + +The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'. + +The list of possible codec options is available in the Android documentation: + + + +.TP +.BI "\-\-video\-encoder " name +Use a specific MediaCodec video encoder (depending on the codec provided by \fB\-\-video\-codec\fR). + +The available encoders can be listed by \fB\-\-list\-encoders\fR. + +.TP +.BI "\-\-video\-source " source +Select the video source (display or camera). + +Camera mirroring requires Android 12+. + +Default is display. + +.TP +.B \-w, \-\-stay-awake +Keep the device on while scrcpy is running, when the device is plugged in. + +.TP +.B \-\-window\-borderless +Disable window decorations (display borderless window). + +.TP +.BI "\-\-window\-title " text +Set a custom window title. + +.TP +.BI "\-\-window\-x " value +Set the initial window horizontal position. + +Default is "auto". + +.TP +.BI "\-\-window\-y " value +Set the initial window vertical position. + +Default is "auto". + +.TP +.BI "\-\-window\-width " value +Set the initial window width. + +Default is 0 (automatic). + +.TP +.BI "\-\-window\-height " value +Set the initial window height. + +Default is 0 (automatic). + +.SH EXIT STATUS +.B scrcpy +will exit with code 0 on normal program termination. If an initial +connection cannot be established, the exit code 1 will be returned. If the +device disconnects while a session is active, exit code 2 will be returned. + +.SH SHORTCUTS + +In the following list, MOD is the shortcut modifier. By default, it's (left) +Alt or (left) Super, but it can be configured by \fB\-\-shortcut\-mod\fR (see above). + +.TP +.B MOD+f +Switch fullscreen mode + +.TP +.B MOD+Left +Rotate display left + +.TP +.B MOD+Right +Rotate display right + +.TP +.B MOD+Shift+Left, MOD+Shift+Right +Flip display horizontally + +.TP +.B MOD+Shift+Up, MOD+Shift+Down +Flip display vertically + +.TP +.B MOD+z +Pause or re-pause display + +.TP +.B MOD+Shift+z +Unpause display + +.TP +.B MOD+Shift+r +Reset video capture/encoding + +.TP +.B MOD+g +Resize window to 1:1 (pixel\-perfect) + +.TP +.B MOD+w, Double\-click on black borders +Resize window to remove black borders + +.TP +.B MOD+h, Home, Middle\-click +Click on HOME + +.TP +.B MOD+b, MOD+Backspace, Right\-click (when screen is on) +Click on BACK + +.TP +.B MOD+s +Click on APP_SWITCH + +.TP +.B MOD+m +Click on MENU + +.TP +.B MOD+Up +Click on VOLUME_UP + +.TP +.B MOD+Down +Click on VOLUME_DOWN + +.TP +.B MOD+p +Click on POWER (turn screen on/off) + +.TP +.B Right\-click (when screen is off) +Turn screen on + +.TP +.B MOD+o +Turn device screen off (keep mirroring) + +.TP +.B MOD+Shift+o +Turn device screen on + +.TP +.B MOD+r +Rotate device screen + +.TP +.B MOD+n +Expand notification panel + +.TP +.B MOD+Shift+n +Collapse notification panel + +.TP +.B Mod+c +Copy to clipboard (inject COPY keycode, Android >= 7 only) + +.TP +.B Mod+x +Cut to clipboard (inject CUT keycode, Android >= 7 only) + +.TP +.B MOD+v +Copy computer clipboard to device, then paste (inject PASTE keycode, Android >= 7 only) + +.TP +.B MOD+Shift+v +Inject computer clipboard text as a sequence of key events + +.TP +.B MOD+k +Open keyboard settings on the device (for HID keyboard only) + +.TP +.B MOD+i +Enable/disable FPS counter (print frames/second in logs) + +.TP +.B Ctrl+click-and-move +Pinch-to-zoom and rotate from the center of the screen + +.TP +.B Shift+click-and-move +Tilt vertically (slide with 2 fingers) + +.TP +.B Ctrl+Shift+click-and-move +Tilt horizontally (slide with 2 fingers) + +.TP +.B Drag & drop APK file +Install APK from computer + +.TP +.B Drag & drop non-APK file +Push file to device (see \fB\-\-push\-target\fR) + + +.SH Environment variables + +.TP +.B ADB +Path to adb. + +.TP +.B ANDROID_SERIAL +Device serial to use if no selector (\fB-s\fR, \fB-d\fR, \fB-e\fR or \fB\-\-tcpip=\fIaddr\fR) is specified. + +.TP +.B SCRCPY_ICON_PATH +Path to the program icon. + +.TP +.B SCRCPY_SERVER_PATH +Path to the server binary. + + +.SH AUTHORS +.B scrcpy +is written by Romain Vimont. + +This manual page was written by +.MT mmyangfl@gmail.com +Yangfl +.ME +for the Debian Project (and may be used by others). + + +.SH "REPORTING BUGS" +Report bugs to . + +.SH COPYRIGHT +Copyright \(co 2018 Genymobile + +Copyright \(co 2018\-2025 Romain Vimont + +Licensed under the Apache License, Version 2.0. + +.SH WWW + diff --git a/copilot_tools/scrcpy/mac/icon.png b/copilot_tools/scrcpy/mac/icon.png new file mode 100644 index 0000000..b96a1af Binary files /dev/null and b/copilot_tools/scrcpy/mac/icon.png differ diff --git a/copilot_tools/scrcpy/mac/scrcpy b/copilot_tools/scrcpy/mac/scrcpy new file mode 100644 index 0000000..550b588 Binary files /dev/null and b/copilot_tools/scrcpy/mac/scrcpy differ diff --git a/copilot_tools/scrcpy/mac/scrcpy-server b/copilot_tools/scrcpy/mac/scrcpy-server new file mode 100644 index 0000000..b36f14d Binary files /dev/null and b/copilot_tools/scrcpy/mac/scrcpy-server differ diff --git a/copilot_tools/scrcpy/mac/scrcpy.1 b/copilot_tools/scrcpy/mac/scrcpy.1 new file mode 100644 index 0000000..d72fda1 --- /dev/null +++ b/copilot_tools/scrcpy/mac/scrcpy.1 @@ -0,0 +1,860 @@ +.TH "scrcpy" "1" +.SH NAME +scrcpy \- Display and control your Android device + + +.SH SYNOPSIS +.B scrcpy +.RI [ options ] + + +.SH DESCRIPTION +.B scrcpy +provides display and control of Android devices connected on USB (or over TCP/IP). It does not require any root access. + + +.SH OPTIONS + +.TP +.B \-\-always\-on\-top +Make scrcpy window always on top (above other windows). + +.TP +.BI "\-\-angle " degrees +Rotate the video content by a custom angle, in degrees (clockwise). + +.TP +.BI "\-\-audio\-bit\-rate " value +Encode the audio at the given bit rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000). + +Default is 128K (128000). + +.TP +.BI "\-\-audio\-buffer " ms +Configure the audio buffering delay (in milliseconds). + +Lower values decrease the latency, but increase the likelihood of buffer underrun (causing audio glitches). + +Default is 50. + +.TP +.BI "\-\-audio\-codec " name +Select an audio codec (opus, aac, flac or raw). + +Default is opus. + +.TP +.BI "\-\-audio\-codec\-options " key\fR[:\fItype\fR]=\fIvalue\fR[,...] +Set a list of comma-separated key:type=value options for the device audio encoder. + +The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'. + +The list of possible codec options is available in the Android documentation: + + + +.TP +.B \-\-audio\-dup +Duplicate audio (capture and keep playing on the device). + +This feature is only available with --audio-source=playback. + +.TP +.BI "\-\-audio\-encoder " name +Use a specific MediaCodec audio encoder (depending on the codec provided by \fB\-\-audio\-codec\fR). + +The available encoders can be listed by \fB\-\-list\-encoders\fR. + +.TP +.BI "\-\-audio\-source " source +Select the audio source. Possible values are: + + - "output": forwards the whole audio output, and disables playback on the device. + - "playback": captures the audio playback (Android apps can opt-out, so the whole output is not necessarily captured). + - "mic": captures the microphone. + - "mic-unprocessed": captures the microphone unprocessed (raw) sound. + - "mic-camcorder": captures the microphone tuned for video recording, with the same orientation as the camera if available. + - "mic-voice-recognition": captures the microphone tuned for voice recognition. + - "mic-voice-communication": captures the microphone tuned for voice communications (it will for instance take advantage of echo cancellation or automatic gain control if available). + - "voice-call": captures voice call. + - "voice-call-uplink": captures voice call uplink only. + - "voice-call-downlink": captures voice call downlink only. + - "voice-performance": captures audio meant to be processed for live performance (karaoke), includes both the microphone and the device playback. + +Default is output. + +.TP +.BI "\-\-audio\-output\-buffer " ms +Configure the size of the SDL audio output buffer (in milliseconds). + +If you get "robotic" audio playback, you should test with a higher value (10). Do not change this setting otherwise. + +Default is 5. + +.TP +.BI "\-b, \-\-video\-bit\-rate " value +Encode the video at the given bit rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000). + +Default is 8M (8000000). + +.TP +.BI "\-\-camera\-ar " ar +Select the camera size by its aspect ratio (+/- 10%). + +Possible values are "sensor" (use the camera sensor aspect ratio), "\fInum\fR:\fIden\fR" (e.g. "4:3") and "\fIvalue\fR" (e.g. "1.6"). + +.TP +.BI "\-\-camera\-facing " facing +Select the device camera by its facing direction. + +Possible values are "front", "back" and "external". + +.TP +.BI "\-\-camera\-fps " fps +Specify the camera capture frame rate. + +If not specified, Android's default frame rate (30 fps) is used. + +.TP +.B \-\-camera\-high\-speed +Enable high-speed camera capture mode. + +This mode is restricted to specific resolutions and frame rates, listed by \fB\-\-list\-camera\-sizes\fR. + +.TP +.BI "\-\-camera\-id " id +Specify the device camera id to mirror. + +The available camera ids can be listed by \fB\-\-list\-cameras\fR. + +.TP +.BI "\-\-camera\-size " width\fRx\fIheight +Specify an explicit camera capture size. + +.TP +.BI "\-\-capture\-orientation " value +Possible values are 0, 90, 180, 270, flip0, flip90, flip180 and flip270, possibly prefixed by '@'. + +The number represents the clockwise rotation in degrees; the "flip" keyword applies a horizontal flip before the rotation. + +If a leading '@' is passed (@90) for display capture, then the rotation is locked, and is relative to the natural device orientation. + +If '@' is passed alone, then the rotation is locked to the initial device orientation. + +Default is 0. + +.TP +.BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy +Crop the device screen on the server. + +The values are expressed in the device natural orientation (typically, portrait for a phone, landscape for a tablet). + +.TP +.B \-d, \-\-select\-usb +Use USB device (if there is exactly one, like adb -d). + +Also see \fB\-e\fR (\fB\-\-select\-tcpip\fR). + +.TP +.BI "\-\-disable\-screensaver" +Disable screensaver while scrcpy is running. + +.TP +.BI "\-\-display\-id " id +Specify the device display id to mirror. + +The available display ids can be listed by \fB\-\-list\-displays\fR. + +Default is 0. + +.TP +.BI "\-\-display\-ime\-policy " value +Set the policy for selecting where the IME should be displayed. + +Possible values are "local", "fallback" and "hide": + + - "local" means that the IME should appear on the local display. + - "fallback" means that the IME should appear on a fallback display (the default display). + - "hide" means that the IME should be hidden. + +By default, the IME policy is left unchanged. + + +.TP +.BI "\-\-display\-orientation " value +Set the initial display orientation. + +Possible values are 0, 90, 180, 270, flip0, flip90, flip180 and flip270. The number represents the clockwise rotation in degrees; the "flip" keyword applies a horizontal flip before the rotation. + +Default is 0. + +.TP +.B \-e, \-\-select\-tcpip +Use TCP/IP device (if there is exactly one, like adb -e). + +Also see \fB\-d\fR (\fB\-\-select\-usb\fR). + +.TP +.B \-f, \-\-fullscreen +Start in fullscreen. + +.TP +.B \-\-force\-adb\-forward +Do not attempt to use "adb reverse" to connect to the device. + +.TP +.B \-G +Same as \fB\-\-gamepad=uhid\fR, or \fB\-\-keyboard=aoa\fR if \fB\-\-otg\fR is set. + +.TP +.BI "\-\-gamepad " mode +Select how to send gamepad inputs to the device. + +Possible values are "disabled", "uhid" and "aoa": + + - "disabled" does not send gamepad inputs to the device. + - "uhid" simulates physical HID gamepads using the Linux HID kernel module on the device. + - "aoa" simulates physical HID gamepads using the AOAv2 protocol. It may only work over USB. + +Also see \fB\-\-keyboard\f and R\fB\-\-mouse\fR. +.TP +.B \-h, \-\-help +Print this help. + +.TP +.B \-K +Same as \fB\-\-keyboard=uhid\fR, or \fB\-\-keyboard=aoa\fR if \fB\-\-otg\fR is set. + +.TP +.BI "\-\-keyboard " mode +Select how to send keyboard inputs to the device. + +Possible values are "disabled", "sdk", "uhid" and "aoa": + + - "disabled" does not send keyboard inputs to the device. + - "sdk" uses the Android system API to deliver keyboard events to applications. + - "uhid" simulates a physical HID keyboard using the Linux HID kernel module on the device. + - "aoa" simulates a physical HID keyboard using the AOAv2 protocol. It may only work over USB. + +For "uhid" and "aoa", the keyboard layout must be configured (once and for all) on the device, via Settings -> System -> Languages and input -> Physical keyboard. This settings page can be started directly using the shortcut MOD+k (except in OTG mode), or by executing: + + adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS + +This option is only available when the HID keyboard is enabled (or a physical keyboard is connected). + +Also see \fB\-\-mouse\fR and \fB\-\-gamepad\fR. + +.TP +.B \-\-kill\-adb\-on\-close +Kill adb when scrcpy terminates. + +.TP +.B \-\-legacy\-paste +Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+Shift+v). + +This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically. + +.TP +.B \-\-list\-apps +List Android apps installed on the device. + +.TP +.B \-\-list\-camera\-sizes +List the valid camera capture sizes. + +.TP +.B \-\-list\-cameras +List cameras available on the device. + +.TP +.B \-\-list\-encoders +List video and audio encoders available on the device. + +.TP +.B \-\-list\-displays +List displays available on the device. + +.TP +.BI "\-m, \-\-max\-size " value +Limit both the width and height of the video to \fIvalue\fR. The other dimension is computed so that the device aspect\-ratio is preserved. + +Default is 0 (unlimited). + +.TP +.B \-M +Same as \fB\-\-mouse=uhid\fR, or \fB\-\-mouse=aoa\fR if \fB\-\-otg\fR is set. + +.TP +.BI "\-\-max\-fps " value +Limit the framerate of screen capture (officially supported since Android 10, but may work on earlier versions). + +.TP +.BI "\-\-mouse " mode +Select how to send mouse inputs to the device. + +Possible values are "disabled", "sdk", "uhid" and "aoa": + + - "disabled" does not send mouse inputs to the device. + - "sdk" uses the Android system API to deliver mouse events to applications. + - "uhid" simulates a physical HID mouse using the Linux HID kernel module on the device. + - "aoa" simulates a physical mouse using the AOAv2 protocol. It may only work over USB. + +In "uhid" and "aoa" modes, the computer mouse is captured to control the device directly (relative mouse mode). + +LAlt, LSuper or RSuper toggle the capture mode, to give control of the mouse back to the computer. + +Also see \fB\-\-keyboard\fR and \fB\-\-gamepad\fR. + +.TP +.BI "\-\-mouse\-bind " xxxx[:xxxx] +Configure bindings of secondary clicks. + +The argument must be one or two sequences (separated by ':') of exactly 4 characters, one for each secondary click (in order: right click, middle click, 4th click, 5th click). + +The first sequence defines the primary bindings, used when a mouse button is pressed alone. The second sequence defines the secondary bindings, used when a mouse button is pressed while the Shift key is held. + +If the second sequence of bindings is omitted, then it is the same as the first one. + +Each character must be one of the following: + + - '+': forward the click to the device + - '-': ignore the click + - 'b': trigger shortcut BACK (or turn screen on if off) + - 'h': trigger shortcut HOME + - 's': trigger shortcut APP_SWITCH + - 'n': trigger shortcut "expand notification panel" + +Default is 'bhsn:++++' for SDK mouse, and '++++:bhsn' for AOA and UHID. + + +.TP +.B \-n, \-\-no\-control +Disable device control (mirror the device in read\-only). + +.TP +.B \-N, \-\-no\-playback +Disable video and audio playback on the computer (equivalent to \fB\-\-no\-video\-playback \-\-no\-audio\-playback\fR). + +.TP +\fB\-\-new\-display\fR[=[\fIwidth\fRx\fIheight\fR][/\fIdpi\fR]] +Create a new display with the specified resolution and density. If not provided, they default to the main display dimensions and DPI. + +Examples: + + \-\-new\-display=1920x1080 + \-\-new\-display=1920x1080/420 + \-\-new\-display # main display size and density + \-\-new\-display=/240 # main display size and 240 dpi + +.TP +.B \-\-no\-audio +Disable audio forwarding. + +.TP +.B \-\-no\-audio\-playback +Disable audio playback on the computer. + +.TP +.B \-\-no\-cleanup +By default, scrcpy removes the server binary from the device and restores the device state (show touches, stay awake and power mode) on exit. + +This option disables this cleanup. + +.TP +.B \-\-no\-clipboard\-autosync +By default, scrcpy automatically synchronizes the computer clipboard to the device clipboard before injecting Ctrl+v, and the device clipboard to the computer clipboard whenever it changes. + +This option disables this automatic synchronization. + +.TP +.B \-\-no\-downsize\-on\-error +By default, on MediaCodec error, scrcpy automatically tries again with a lower definition. + +This option disables this behavior. + +.TP +.B \-\-no\-key\-repeat +Do not forward repeated key events when a key is held down. + +.TP +.B \-\-no\-mipmaps +If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then mipmaps are automatically generated to improve downscaling quality. This option disables the generation of mipmaps. + +.TP +.B \-\-no\-mouse\-hover +Do not forward mouse hover (mouse motion without any clicks) events. + +.TP +.B \-\-no\-power\-on +Do not power on the device on start. + +.TP +.B \-\-no\-vd\-destroy\-content +Disable virtual display "destroy content on removal" flag. + +With this option, when the virtual display is closed, the running apps are moved to the main display rather than being destroyed. + +.TP +.B \-\-no\-vd\-system\-decorations +Disable virtual display system decorations flag. + +.TP +.B \-\-no\-video +Disable video forwarding. + +.TP +.B \-\-no\-video\-playback +Disable video playback on the computer. + +.TP +.B \-\-no\-window +Disable scrcpy window. Implies --no-video-playback. + +.TP +.BI "\-\-orientation " value +Same as --display-orientation=value --record-orientation=value. + +.TP +.B \-\-otg +Run in OTG mode: simulate physical keyboard and mouse, as if the computer keyboard and mouse were plugged directly to the device via an OTG cable. + +In this mode, adb (USB debugging) is not necessary, and mirroring is disabled. + +LAlt, LSuper or RSuper toggle the mouse capture mode, to give control of the mouse back to the computer. + +If any of \fB\-\-hid\-keyboard\fR or \fB\-\-hid\-mouse\fR is set, only enable keyboard or mouse respectively, otherwise enable both. + +It may only work over USB. + +See \fB\-\-keyboard\fR, \fB\-\-mouse\fR and \fB\-\-gamepad\fR. + +.TP +.BI "\-p, \-\-port " port\fR[:\fIport\fR] +Set the TCP port (range) used by the client to listen. + +Default is 27183:27199. + +.TP +\fB\-\-pause\-on\-exit\fR[=\fImode\fR] +Configure pause on exit. Possible values are "true" (always pause on exit), "false" (never pause on exit) and "if-error" (pause only if an error occurred). + +This is useful to prevent the terminal window from automatically closing, so that error messages can be read. + +Default is "false". + +Passing the option without argument is equivalent to passing "true". + +.TP +.B \-\-power\-off\-on\-close +Turn the device screen off when closing scrcpy. + +.TP +.B \-\-prefer\-text +Inject alpha characters and space as text events instead of key events. + +This avoids issues when combining multiple keys to enter special characters, +but breaks the expected behavior of alpha keys in games (typically WASD). + +.TP +.B "\-\-print\-fps +Start FPS counter, to print framerate logs to the console. It can be started or stopped at any time with MOD+i. + +.TP +.BI "\-\-push\-target " path +Set the target directory for pushing files to the device by drag & drop. It is passed as\-is to "adb push". + +Default is "/sdcard/Download/". + +.TP +.BI "\-r, \-\-record " file +Record screen to +.IR file . + +The format is determined by the +.B \-\-record\-format +option if set, or by the file extension. + +.TP +.B \-\-raw\-key\-events +Inject key events for all input keys, and ignore text events. + +.TP +.BI "\-\-record\-format " format +Force recording format (mp4, mkv, m4a, mka, opus, aac, flac or wav). + +.TP +.BI "\-\-record\-orientation " value +Set the record orientation. + +Possible values are 0, 90, 180 and 270. The number represents the clockwise rotation in degrees. + +Default is 0. + +.TP +.BI "\-\-render\-driver " name +Request SDL to use the given render driver (this is just a hint). + +Supported names are currently "direct3d", "opengl", "opengles2", "opengles", "metal" and "software". + + + +.TP +.B \-\-require\-audio +By default, scrcpy mirrors only the video if audio capture fails on the device. This option makes scrcpy fail if audio is enabled but does not work. + +.TP +.BI "\-s, \-\-serial " number +The device serial number. Mandatory only if several devices are connected to adb. + +.TP +.B \-S, \-\-turn\-screen\-off +Turn the device screen off immediately. + +.TP +.B "\-\-screen\-off\-timeout " seconds +Set the screen off timeout while scrcpy is running (restore the initial value on exit). + +.TP +.BI "\-\-shortcut\-mod " key\fR[+...]][,...] +Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper". + +Several shortcut modifiers can be specified, separated by ','. + +For example, to use either LCtrl or LSuper for scrcpy shortcuts, pass "lctrl,lsuper". + +Default is "lalt,lsuper" (left-Alt or left-Super). + +.TP +.BI "\-\-start\-app " name +Start an Android app, by its exact package name. + +Add a '?' prefix to select an app whose name starts with the given name, case-insensitive (retrieving app names on the device may take some time): + + scrcpy --start-app=?firefox + +Add a '+' prefix to force-stop before starting the app: + + scrcpy --new-display --start-app=+org.mozilla.firefox + +Both prefixes can be used, in that order: + + scrcpy --start-app=+?firefox + +.TP +.B \-t, \-\-show\-touches +Enable "show touches" on start, restore the initial value on exit. + +It only shows physical touches (not clicks from scrcpy). + +.TP +.BI "\-\-tcpip\fR[=[+]\fIip\fR[:\fIport\fR]] +Configure and connect the device over TCP/IP. + +If a destination address is provided, then scrcpy connects to this address before starting. The device must listen on the given TCP port (default is 5555). + +If no destination address is provided, then scrcpy attempts to find the IP address and adb port of the current device (typically connected over USB), enables TCP/IP mode if necessary, then connects to this address before starting. + +Prefix the address with a '+' to force a reconnection. + +.TP +.BI "\-\-time\-limit " seconds +Set the maximum mirroring time, in seconds. + +.TP +.BI "\-\-tunnel\-host " ip +Set the IP address of the adb tunnel to reach the scrcpy server. This option automatically enables \fB\-\-force\-adb\-forward\fR. + +Default is localhost. + +.TP +.BI "\-\-tunnel\-port " port +Set the TCP port of the adb tunnel to reach the scrcpy server. This option automatically enables \fB\-\-force\-adb\-forward\fR. + +Default is 0 (not forced): the local port used for establishing the tunnel will be used. + +.TP +.B \-v, \-\-version +Print the version of scrcpy. + +.TP +.BI "\-V, \-\-verbosity " value +Set the log level ("verbose", "debug", "info", "warn" or "error"). + +Default is "info" for release builds, "debug" for debug builds. + +.TP +.BI "\-\-v4l2-sink " /dev/videoN +Output to v4l2loopback device. + +.TP +.BI "\-\-v4l2-buffer " ms +Add a buffering delay (in milliseconds) before pushing frames. This increases latency to compensate for jitter. + +This option is similar to \fB\-\-video\-buffer\fR, but specific to V4L2 sink. + +Default is 0 (no buffering). + +.TP +.BI "\-\-video\-buffer " ms +Add a buffering delay (in milliseconds) before displaying video frames. + +This increases latency to compensate for jitter. + +Default is 0 (no buffering). + +.TP +.BI "\-\-video\-codec " name +Select a video codec (h264, h265 or av1). + +Default is h264. + +.TP +.BI "\-\-video\-codec\-options " key\fR[:\fItype\fR]=\fIvalue\fR[,...] +Set a list of comma-separated key:type=value options for the device video encoder. + +The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'. + +The list of possible codec options is available in the Android documentation: + + + +.TP +.BI "\-\-video\-encoder " name +Use a specific MediaCodec video encoder (depending on the codec provided by \fB\-\-video\-codec\fR). + +The available encoders can be listed by \fB\-\-list\-encoders\fR. + +.TP +.BI "\-\-video\-source " source +Select the video source (display or camera). + +Camera mirroring requires Android 12+. + +Default is display. + +.TP +.B \-w, \-\-stay-awake +Keep the device on while scrcpy is running, when the device is plugged in. + +.TP +.B \-\-window\-borderless +Disable window decorations (display borderless window). + +.TP +.BI "\-\-window\-title " text +Set a custom window title. + +.TP +.BI "\-\-window\-x " value +Set the initial window horizontal position. + +Default is "auto". + +.TP +.BI "\-\-window\-y " value +Set the initial window vertical position. + +Default is "auto". + +.TP +.BI "\-\-window\-width " value +Set the initial window width. + +Default is 0 (automatic). + +.TP +.BI "\-\-window\-height " value +Set the initial window height. + +Default is 0 (automatic). + +.SH EXIT STATUS +.B scrcpy +will exit with code 0 on normal program termination. If an initial +connection cannot be established, the exit code 1 will be returned. If the +device disconnects while a session is active, exit code 2 will be returned. + +.SH SHORTCUTS + +In the following list, MOD is the shortcut modifier. By default, it's (left) +Alt or (left) Super, but it can be configured by \fB\-\-shortcut\-mod\fR (see above). + +.TP +.B MOD+f +Switch fullscreen mode + +.TP +.B MOD+Left +Rotate display left + +.TP +.B MOD+Right +Rotate display right + +.TP +.B MOD+Shift+Left, MOD+Shift+Right +Flip display horizontally + +.TP +.B MOD+Shift+Up, MOD+Shift+Down +Flip display vertically + +.TP +.B MOD+z +Pause or re-pause display + +.TP +.B MOD+Shift+z +Unpause display + +.TP +.B MOD+Shift+r +Reset video capture/encoding + +.TP +.B MOD+g +Resize window to 1:1 (pixel\-perfect) + +.TP +.B MOD+w, Double\-click on black borders +Resize window to remove black borders + +.TP +.B MOD+h, Home, Middle\-click +Click on HOME + +.TP +.B MOD+b, MOD+Backspace, Right\-click (when screen is on) +Click on BACK + +.TP +.B MOD+s +Click on APP_SWITCH + +.TP +.B MOD+m +Click on MENU + +.TP +.B MOD+Up +Click on VOLUME_UP + +.TP +.B MOD+Down +Click on VOLUME_DOWN + +.TP +.B MOD+p +Click on POWER (turn screen on/off) + +.TP +.B Right\-click (when screen is off) +Turn screen on + +.TP +.B MOD+o +Turn device screen off (keep mirroring) + +.TP +.B MOD+Shift+o +Turn device screen on + +.TP +.B MOD+r +Rotate device screen + +.TP +.B MOD+n +Expand notification panel + +.TP +.B MOD+Shift+n +Collapse notification panel + +.TP +.B Mod+c +Copy to clipboard (inject COPY keycode, Android >= 7 only) + +.TP +.B Mod+x +Cut to clipboard (inject CUT keycode, Android >= 7 only) + +.TP +.B MOD+v +Copy computer clipboard to device, then paste (inject PASTE keycode, Android >= 7 only) + +.TP +.B MOD+Shift+v +Inject computer clipboard text as a sequence of key events + +.TP +.B MOD+k +Open keyboard settings on the device (for HID keyboard only) + +.TP +.B MOD+i +Enable/disable FPS counter (print frames/second in logs) + +.TP +.B Ctrl+click-and-move +Pinch-to-zoom and rotate from the center of the screen + +.TP +.B Shift+click-and-move +Tilt vertically (slide with 2 fingers) + +.TP +.B Ctrl+Shift+click-and-move +Tilt horizontally (slide with 2 fingers) + +.TP +.B Drag & drop APK file +Install APK from computer + +.TP +.B Drag & drop non-APK file +Push file to device (see \fB\-\-push\-target\fR) + + +.SH Environment variables + +.TP +.B ADB +Path to adb. + +.TP +.B ANDROID_SERIAL +Device serial to use if no selector (\fB-s\fR, \fB-d\fR, \fB-e\fR or \fB\-\-tcpip=\fIaddr\fR) is specified. + +.TP +.B SCRCPY_ICON_PATH +Path to the program icon. + +.TP +.B SCRCPY_SERVER_PATH +Path to the server binary. + + +.SH AUTHORS +.B scrcpy +is written by Romain Vimont. + +This manual page was written by +.MT mmyangfl@gmail.com +Yangfl +.ME +for the Debian Project (and may be used by others). + + +.SH "REPORTING BUGS" +Report bugs to . + +.SH COPYRIGHT +Copyright \(co 2018 Genymobile + +Copyright \(co 2018\-2025 Romain Vimont + +Licensed under the Apache License, Version 2.0. + +.SH WWW + diff --git a/copilot_tools/scrcpy/win/SDL2.dll b/copilot_tools/scrcpy/win/SDL2.dll new file mode 100644 index 0000000..82d2b1a Binary files /dev/null and b/copilot_tools/scrcpy/win/SDL2.dll differ diff --git a/copilot_tools/scrcpy/win/avcodec-61.dll b/copilot_tools/scrcpy/win/avcodec-61.dll new file mode 100644 index 0000000..a44b6c6 Binary files /dev/null and b/copilot_tools/scrcpy/win/avcodec-61.dll differ diff --git a/copilot_tools/scrcpy/win/avformat-61.dll b/copilot_tools/scrcpy/win/avformat-61.dll new file mode 100644 index 0000000..347e657 Binary files /dev/null and b/copilot_tools/scrcpy/win/avformat-61.dll differ diff --git a/copilot_tools/scrcpy/win/avutil-59.dll b/copilot_tools/scrcpy/win/avutil-59.dll new file mode 100644 index 0000000..9b847e3 Binary files /dev/null and b/copilot_tools/scrcpy/win/avutil-59.dll differ diff --git a/copilot_tools/scrcpy/win/icon.png b/copilot_tools/scrcpy/win/icon.png new file mode 100644 index 0000000..b96a1af Binary files /dev/null and b/copilot_tools/scrcpy/win/icon.png differ diff --git a/copilot_tools/scrcpy/win/libusb-1.0.dll b/copilot_tools/scrcpy/win/libusb-1.0.dll new file mode 100644 index 0000000..b36c945 Binary files /dev/null and b/copilot_tools/scrcpy/win/libusb-1.0.dll differ diff --git a/copilot_tools/scrcpy/win/open_a_terminal_here.bat b/copilot_tools/scrcpy/win/open_a_terminal_here.bat new file mode 100644 index 0000000..24d557f --- /dev/null +++ b/copilot_tools/scrcpy/win/open_a_terminal_here.bat @@ -0,0 +1 @@ +@cmd diff --git a/copilot_tools/scrcpy/win/scrcpy-console.bat b/copilot_tools/scrcpy/win/scrcpy-console.bat new file mode 100644 index 0000000..0ea7619 --- /dev/null +++ b/copilot_tools/scrcpy/win/scrcpy-console.bat @@ -0,0 +1,2 @@ +@echo off +scrcpy.exe --pause-on-exit=if-error %* diff --git a/copilot_tools/scrcpy/win/scrcpy-noconsole.vbs b/copilot_tools/scrcpy/win/scrcpy-noconsole.vbs new file mode 100644 index 0000000..d509ad7 --- /dev/null +++ b/copilot_tools/scrcpy/win/scrcpy-noconsole.vbs @@ -0,0 +1,7 @@ +strCommand = "cmd /c scrcpy.exe" + +For Each Arg In WScript.Arguments + strCommand = strCommand & " """ & replace(Arg, """", """""""""") & """" +Next + +CreateObject("Wscript.Shell").Run strCommand, 0, false diff --git a/copilot_tools/scrcpy/win/scrcpy-server b/copilot_tools/scrcpy/win/scrcpy-server new file mode 100644 index 0000000..b36f14d Binary files /dev/null and b/copilot_tools/scrcpy/win/scrcpy-server differ diff --git a/copilot_tools/scrcpy/win/scrcpy.exe b/copilot_tools/scrcpy/win/scrcpy.exe new file mode 100644 index 0000000..496ec24 Binary files /dev/null and b/copilot_tools/scrcpy/win/scrcpy.exe differ diff --git a/copilot_tools/scrcpy/win/swresample-5.dll b/copilot_tools/scrcpy/win/swresample-5.dll new file mode 100644 index 0000000..49d9cb0 Binary files /dev/null and b/copilot_tools/scrcpy/win/swresample-5.dll differ diff --git a/examples/visualization_group_control/run_single_task_group_control_Job_Scheduler.py b/examples/visualization_group_control/run_single_task_group_control_Job_Scheduler.py new file mode 100644 index 0000000..16c9153 --- /dev/null +++ b/examples/visualization_group_control/run_single_task_group_control_Job_Scheduler.py @@ -0,0 +1,424 @@ +import os +import sys +import time +import subprocess +import platform +import re +import threading + +if "." not in sys.path: + sys.path.append(".") + +from copilot_agent_client.pu_client import evaluate_task_on_device +from copilot_front_end.mobile_action_helper import list_devices, get_device_wm_size +from copilot_agent_server.local_server import LocalServer + +tmp_server_config = { + "log_dir": "running_log/server_log/os-copilot-local-eval-logs/traces", + "image_dir": "running_log/server_server/os-copilot-local-eval-logs/images", + "debug": False +} + + +local_model_config = { + "task_type": "parser_0922_summary", + "model_config": { + "model_name": "gelab-zero-4b-preview", + "model_provider": "local", + "args": { + "temperature": 0.1, + "top_p": 0.95, + "frequency_penalty": 0.0, + "max_tokens": 4096, + }, + }, + + "max_steps": 400, + "delay_after_capture": 2, + "debug": False +} + +# ===== 配置变量定义 (全局作用域) ===== +tmp_rollout_config = local_model_config + +# ===== 中央配置:设备能力与任务队列===== +DEVICE_CAPABILITIES = { + "FIRST_DEVICE": {"tag": "SHOPPING_CONSUMPTION"}, + "SECOND_DEVICE": {"tag": "VIDEO_STREAMING"}, +} + +JOB_QUEUE = [ + {"task": "在淘宝上搜索并下单一个键盘", "required_tag": "SHOPPING_CONSUMPTION"}, + {"task": "打开爱奇艺,播放《疯狂动物城》", "required_tag": "VIDEO_STREAMING"}, + {"task": "检查微信是否有未读消息", "required_tag": "SOCIAL_MEDIA"}, +] + +# ===== 用于记录每步耗时 ===== +_step_times = [] + +# 全局字典,用于存储 scrcpy 的实际 PID (而非 Shell/CMD 的 PID) +scrcpy_pids_to_kill = {} + + +# ===== 包装 automate_step 方法 ===== +def wrap_automate_step_with_timing(server_instance): + original_method = server_instance.automate_step + + def timed_automate_step(payload): + step_start = time.time() + try: + result = original_method(payload) + finally: + duration = time.time() - step_start + _step_times.append(duration) + print(f"[Thread {threading.get_ident()}] Step {len(_step_times)} took: {duration:.2f} seconds") + return result + + server_instance.automate_step = timed_automate_step + +# ===== 等待目标设备重新稳定上线 ===== +def wait_for_device_stability(target_device_ids, timeout=10, interval=0.5): + print(f"\n等待 {len(target_device_ids)} 个设备在 ADB 中稳定...") + start_time = time.time() + + devices_to_wait_for = set(target_device_ids) + + while time.time() - start_time < timeout and devices_to_wait_for: + try: + connected_devices = list_devices() + + stable_now = devices_to_wait_for.intersection(set(connected_devices)) + if stable_now: + print(f" -> 设备 {list(stable_now)} 已稳定连接。") + devices_to_wait_for -= stable_now + + except Exception: + pass + + if devices_to_wait_for: + time.sleep(interval) + + if devices_to_wait_for: + print(f"错误:等待设备 {list(devices_to_wait_for)} 超时 ({timeout}秒),任务终止。") + return False + + print("所有目标设备已稳定连接。") + return True + +# ===== 在单个设备上运行任务的封装(移除线程内清理,仅执行任务) ===== +def run_task_on_device(device_id, tmp_server_config, task): + global tmp_rollout_config + + l2_server = LocalServer(tmp_server_config) + wrap_automate_step_with_timing(l2_server) + + try: + device_wm_size = get_device_wm_size(device_id) + device_info = { + "device_id": device_id, + "device_wm_size": device_wm_size + } + + print(f"\n>>>> 任务开始在设备: {device_id} 上执行 (任务: {task[:10]}...) <<<<") + evaluate_task_on_device(l2_server, device_info, task, tmp_rollout_config, reflush_app=True) + print(f">>>> 任务执行完毕 (设备: {device_id}) <<<<\n") + + except Exception as e: + print(f"任务执行过程中发生错误 (设备: {device_id}): {e}") + + finally: + pass + + +# ===== scrcpy 基础路径定义 ===== +SCRCPY_BASE_DIR = os.path.join("copilot_tools", "scrcpy") + + +# ===== 动态获取 scrcpy 路径 ===== +def get_scrcpy_path(): + """根据操作系统,返回 SCRCPY_BASE_DIR 文件夹中的 scrcpy 可执行文件路径""" + + system = platform.system() + + if system == "Windows": + path = os.path.join(SCRCPY_BASE_DIR, "win", "scrcpy.exe") + elif system == "Darwin": # macOS + path = os.path.join(SCRCPY_BASE_DIR, "mac", "scrcpy") + elif system == "Linux": + path = os.path.join(SCRCPY_BASE_DIR, "linux", "scrcpy") + else: + raise OSError(f"不支持的操作系统: {system}") + + + if not os.path.exists(path): + if system == "Linux": + try: + subprocess.run(["scrcpy", "-h"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + return "scrcpy" + except (subprocess.CalledProcessError, FileNotFoundError): + pass + + raise FileNotFoundError(f"scrcpy 可执行文件未找到,请检查路径: {os.path.abspath(path)}") + + return os.path.abspath(path) + +# ===== 【新增】获取当前所有 scrcpy 进程的 PID 集合 (跨平台) ===== +def get_scrcpy_pids(scrcpy_path): + system = platform.system() + pids = set() + + if system == "Windows": + try: + # 查找所有名为 scrcpy.exe 的进程 + result = subprocess.run('tasklist /FI "IMAGENAME eq scrcpy.exe" /NH', capture_output=True, text=True, shell=True, check=False) + + for line in result.stdout.strip().split('\n'): + if line.strip(): + parts = line.split() + try: + pids.add(int(parts[1])) + except (ValueError, IndexError): + continue + except Exception: + pass + + elif system in ["Darwin", "Linux"]: + try: + # 查找所有包含 scrcpy 路径的进程 + result = subprocess.run(f'pgrep -f "{scrcpy_path}"', capture_output=True, text=True, shell=True, check=False) + + for pid_str in result.stdout.strip().split('\n'): + if pid_str.strip(): + try: + pids.add(int(pid_str)) + except ValueError: + continue + except Exception: + pass + + return pids + + +# ===== 通过端口查找并终止 PID (跨平台兼容) ===== +def terminate_process_by_port(port): + """使用 Windows 的 netstat/taskkill 或 macOS/Linux 的 lsof/kill 终止占用指定端口的进程""" + system = platform.system() + + if system == "Windows": + # Windows 逻辑 (使用 netstat 和 taskkill) + try: + command = f'netstat -ano | findstr LISTEN | findstr :{port}' + result = subprocess.run(command, capture_output=True, text=True, shell=True, check=False) + + if result.returncode != 0 or not result.stdout.strip(): + return + + pid_match = re.search(r'\s+(\d+)\s*$', result.stdout.strip().split('\n')[0]) + + if pid_match: + pid = pid_match.group(1) + print(f" -> [保险清理] 终止 PID {pid} (端口 {port})...") + # 使用 taskkill /F /T 终止进程树 + subprocess.run(f"taskkill /PID {pid} /F /T", shell=True, check=False, capture_output=True) + return + + except Exception as e: + print(f" -> [保险清理] 终止端口 {port} 失败 (Windows): {e}") + + elif system in ["Darwin", "Linux"]: + # macOS/Linux 逻辑 (使用 lsof 和 kill) + try: + command = f'lsof -i tcp:{port} | grep LISTEN' + result = subprocess.run(command, capture_output=True, text=True, shell=True, check=False) + + if not result.stdout.strip(): + return + + pid_match = re.search(r'\s+(\d+)\s+', result.stdout.strip().split('\n')[0]) + + if pid_match: + pid = pid_match.group(1) + print(f" -> [保险清理] 终止 PID {pid} (端口 {port})...") + subprocess.run(f"kill -9 {pid}", shell=True, check=False, capture_output=True) + return + + except Exception as e: + print(f" -> [保险清理] 终止端口 {port} 失败 (macOS/Linux): {e}") + +# ============================================================= + +if __name__ == "__main__": + # 1. 获取所有设备信息 + all_device_ids = list_devices() + if not all_device_ids: + print("未检测到任何 ADB 设备,请检查设备连接!") + sys.exit(1) + + + # 构建动态设备能力映射,并获取需要启动 scrcpy 的设备列表 + devices_to_start_scrcpy = [] + + temp_device_map = DEVICE_CAPABILITIES.copy() + DEVICE_CAPABILITIES = {} + + # 将配置中的占位符ID替换为实际的在线设备ID + if len(all_device_ids) >= 1: + DEVICE_CAPABILITIES[all_device_ids[0]] = temp_device_map.pop("FIRST_DEVICE") + devices_to_start_scrcpy.append(all_device_ids[0]) + if len(all_device_ids) >= 2: + DEVICE_CAPABILITIES[all_device_ids[1]] = temp_device_map.pop("SECOND_DEVICE") + devices_to_start_scrcpy.append(all_device_ids[1]) + + + scrcpy_ports = {} + # 使用全局的 scrcpy_pids_to_kill 字典 + + total_start = time.time() + task_threads = [] + + START_X = 50 + + try: + # 2. 动态获取 scrcpy 路径 + SCRCPY_PATH = get_scrcpy_path() + SCRCPY_DIR = os.path.dirname(SCRCPY_PATH) + print(f"scrcpy 路径确定为: {SCRCPY_PATH}") + + system = platform.system() + + # 3. 循环启动所有设备的 scrcpy 进程 + print(f"检测到 {len(devices_to_start_scrcpy)} 个配置的设备,正在启动 scrcpy 窗口...") + + #在启动前记录所有 scrcpy 进程的 PID + initial_scrcpy_pids = get_scrcpy_pids(SCRCPY_PATH) + + start_port = 27183 + offset_y = 50 + OFFSET_STEP = 65 + + for i, device_id in enumerate(devices_to_start_scrcpy): + window_position_arg = f"--window-x {START_X} --window-y {offset_y}" + port_arg = f"-p {start_port}" + + # 跨平台启动命令的构建 + command_args = f'"{SCRCPY_PATH}" -s {device_id} {port_arg} {window_position_arg}' + + if system == "Windows": + # 使用 start "" 来分离子进程 + scrcpy_command_str = f'start "" {command_args}' + else: + # 使用 nohup ... & 来分离子进程 + scrcpy_command_str = f'nohup {command_args} > /dev/null 2>&1 &' + + # 启动 Shell/CMD 进程 + subprocess.Popen( + scrcpy_command_str, + shell=True, + ) + + # 等待 scrcpy 窗口启动,并找到其真实的 PID + time.sleep(1.5) # 给予 scrcpy 足够的启动时间 + + current_scrcpy_pids = get_scrcpy_pids(SCRCPY_PATH) + new_pids = current_scrcpy_pids - initial_scrcpy_pids + + if new_pids: + # 找到新启动的 PID (通常只有一个) + actual_pid = list(new_pids)[0] + # 记录实际 PID 用于后续终止 + scrcpy_pids_to_kill[device_id] = actual_pid + initial_scrcpy_pids = current_scrcpy_pids # 更新初始列表供下一个设备使用 + print(f"设备 {device_id} 的 scrcpy 进程已启动 (端口: {start_port}, 实际PID: {actual_pid})...") + else: + print(f"警告: 未能找到设备 {device_id} 的 scrcpy 进程的实际 PID!") + + scrcpy_ports[device_id] = start_port + + offset_y += OFFSET_STEP + start_port += 1 + + if len(devices_to_start_scrcpy) > 1: + time.sleep(0.5) + + # 4. 在执行任务前,等待 ADB 稳定 + if not wait_for_device_stability(devices_to_start_scrcpy): + raise Exception("部分 ADB 连接未能稳定,任务终止。") + + # 5. 任务分配与并发执行 + assignment_batch = [] + available_devices = set(devices_to_start_scrcpy) + + for job in JOB_QUEUE: + required_tag = job['required_tag'] + assigned_now = None + + for device_id in available_devices: + if DEVICE_CAPABILITIES.get(device_id, {}).get('tag') == required_tag: + + assignment_batch.append({ + "device_id": device_id, + "task": job['task'], + "port": scrcpy_ports[device_id] + }) + assigned_now = device_id + break + + if assigned_now: + available_devices.remove(assigned_now) + + + if not assignment_batch: + print("\n没有找到匹配的任务和设备上下文,跳过任务执行。") + else: + print(f"\n>>>> 正在 {len(assignment_batch)} 个设备上并发执行任务 (精确匹配模式) <<<<") + + for assignment in assignment_batch: + device_id = assignment['device_id'] + task = assignment['task'] + + thread = threading.Thread( + target=run_task_on_device, + args=(device_id, tmp_server_config, task) + ) + task_threads.append(thread) + thread.start() + + for thread in task_threads: + thread.join() + + print("\n>>>> 所有并发任务执行完毕 <<<<") + + except FileNotFoundError as e: + print(f"致命错误: {e}") + + except Exception as e: + print(f"任务执行过程中发生错误: {e}") + + finally: + # 6. 终止所有 scrcpy 进程和计时 + total_time = time.time() - total_start + print(f"总计执行时间为 {total_time:.2f} 秒") + + print("\n正在执行最终清理...") + + # 通过真实的 scrcpy PID 终止进程 (最可靠的方式) + for device_id, pid in scrcpy_pids_to_kill.items(): + print(f" -> 尝试通过实际 PID 终止设备 {device_id} 的 scrcpy 进程 (PID: {pid})...") + try: + if platform.system() == "Windows": + # Windows 上使用 taskkill /F /T 来终止进程树 + subprocess.run(f"taskkill /PID {pid} /F /T", shell=True, check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + else: + # macOS/Linux 上使用 kill -9 终止进程 + subprocess.run(f"kill -9 {pid}", shell=True, check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + print(f" -> 设备 {device_id} 的 scrcpy 进程已终止。") + except Exception as e: + print(f" -> 终止 scrcpy 进程失败 ({device_id}): {e}") + + # 保险措施:通过端口终止残留进程 + print("\n -> 执行端口进程的二次保险清理...") + for port in scrcpy_ports.values(): + terminate_process_by_port(port) + + pass diff --git a/mcp_server/Chinese_chess/__init__.py b/mcp_server/Chinese_chess/__init__.py new file mode 100644 index 0000000..07eb4d9 --- /dev/null +++ b/mcp_server/Chinese_chess/__init__.py @@ -0,0 +1 @@ +# Chinese Chess MCP Server diff --git a/mcp_server/Chinese_chess/chess_mcp_server.py b/mcp_server/Chinese_chess/chess_mcp_server.py new file mode 100644 index 0000000..681fc01 --- /dev/null +++ b/mcp_server/Chinese_chess/chess_mcp_server.py @@ -0,0 +1,676 @@ +""" +中国象棋 MCP 服务器 + +架构设计: +1. prepare_game: GeLab 导航到棋盘界面 +2. get_board_state: 视觉模型解析棋盘,返回可视化棋盘图(给外部 GLM 深度思考) +3. execute_chess_move: 外部 GLM 深度思考后,GeLab 执行点击操作 + +关键:外部 GLM 必须先深度思考分析,得出明确结论后,再调用执行工具 +""" + +from fastmcp import FastMCP +import sys +import os +import time +import re +import json + +if "." not in sys.path: + sys.path.append(".") + +project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +if project_root not in sys.path: + sys.path.insert(0, project_root) + +from copilot_front_end.mobile_action_helper import ( + list_devices, + get_device_wm_size, + capture_screenshot, + open_screen, +) +from copilot_agent_client.pu_client import evaluate_task_on_device +from tools.image_tools import make_b64_url +from tools.ask_llm_v2 import ask_llm_anything + +from typing import Annotated +from pydantic import Field +from megfile import smart_remove + +# ============== 绝杀规则 ============== +CHECKMATE_RULES = """ +【绝杀技巧】 +1. 白脸将:将帅同列无子阻挡 +2. 马后炮:马将军+炮在后 +3. 卧槽马:马在九宫角将军 +4. 重炮:双炮同列前炮将军 +5. 闷宫:堵死将的退路 + +【优先级】绝杀 > 吃大子(车>马=炮) > 改善局势 +""" + +# ============== 棋盘解析提示词 ============== +BOARD_ANALYSIS_PROMPT = """请分析这张中国象棋棋盘截图。 + +## 重要:中国象棋的棋子放在交叉点上! + +中国象棋棋盘是由横线和竖线组成的网格,棋子放在**线的交叉点**上,不是格子里面! +- 横线:共10条(从下到上编号 0-9) +- 竖线:共9条(从左到右编号 A-I) +- 棋子位置 = 某条横线和某条竖线的交叉点 + +## 坐标系统 +- 列:A-I(从左到右,共9条竖线) +- 行:0-9(从下到上,共10条横线) +- 坐标格式:列+行,如 E0、A9 +- E0 = 第E列与第0行的交叉点(红方底线正中间) +- E9 = 第E列与第9行的交叉点(黑方底线正中间) + +## 标准初始位置参考 +红方(第0-4行): +- 帅: E0(底线正中) +- 仕: D0, F0 +- 相: C0, G0 +- 马: B0, H0 +- 车: A0, I0 +- 炮: B2, H2 +- 兵: A3, C3, E3, G3, I3 + +黑方(第5-9行): +- 将: E9(顶线正中) +- 士: D9, F9 +- 象: C9, G9 +- 马: B9, H9 +- 车: A9, I9 +- 炮: B7, H7 +- 卒: A6, C6, E6, G6, I6 + +## 棋子识别 +- 红方:帅(K)、仕(A)、相(B)、马(N)、车(R)、炮(C)、兵(P) +- 黑方:将(k)、士(a)、象(b)、马(n)、车(r)、炮(c)、卒(p) + +## 输出格式(严格JSON) +```json +{ + "pieces": [ + {"type": "K", "pos": "E0", "name": "帅"}, + {"type": "R", "pos": "A0", "name": "车"}, + {"type": "k", "pos": "E9", "name": "将"}, + {"type": "r", "pos": "I9", "name": "车"} + ], + "game_status": "playing" +} +``` + +## game_status +- "playing": 对局中 +- "victory": 看到胜利/成功/恭喜提示 +- "defeat": 看到失败提示 + +只输出JSON。 +""" + +# ============== MCP 服务器 ============== +mcp = FastMCP( + name="Chinese-Chess-MCP-Server", + instructions=""" +你是一个中国象棋AI,通过 MCP 工具指挥 GeLab 在手机上下棋。 + +## ⚠️ 核心规则:必须先深度思考! + +当你收到棋盘状态后,**禁止立即调用工具**!你必须: + +### 第一步:深度思考(在你的回复中完成,不调用任何工具) + +请按以下格式进行分析: + +``` +【棋局分析】 +1. 红方棋子:... +2. 黑方棋子:... +3. 当前局势:... + +【绝杀检查】 +- 白脸将:是否可行? +- 马后炮:是否可行? +- 其他绝杀:... + +【可选走法】 +1. 走法A:... 优点/缺点 +2. 走法B:... 优点/缺点 +3. 走法C:... 优点/缺点 + +【最终决定】 +我选择:[具体走法] +理由:... +``` + +### 第二步:确定走法后,调用 execute_chess_move() + +只有在完成上述深度思考,并明确写出"我选择:xxx"后,才能调用工具。 + +## 工作流程 + +1. `prepare_game(task)` - GeLab 导航到棋盘 +2. `get_board_state()` - 获取棋盘(返回可视化棋盘图) +3. **你深度思考分析**(按上述格式) +4. **写出明确结论**:"我选择:xxx" +5. `execute_chess_move(instruction)` - GeLab 执行 +6. 重复 2-5 + +## execute_chess_move 指令格式 + +使用坐标描述走法: +- "把 E0 的帅移动到 E1" +- "把 A0 的车移动到 A9 吃掉黑车" +- "把 B2 的炮移动到 E2" + +或用自然语言: +- "红车从左下角向上直走到顶" +- "红炮平到中路" +""" +) + +# 全局状态 +_current_device_id = None +_device_wm_size = None +_local_server = None +_last_board_state = None + +# GeLab 配置 +GELAB_CONFIG = { + "task_type": "parser_0922_summary", + "model_config": { + "model_name": "step-gui", + "model_provider": "stepfun", + "args": {"temperature": 0.1, "top_p": 0.95, "max_tokens": 4096}, + }, + "max_steps": 30, + "delay_after_capture": 3, +} + +# 视觉模型配置 +VISION_MODEL_CONFIG = { + "model_provider": "stepfun", + "model_name": "step-1v-8k", +} + +SERVER_CONFIG = { + "log_dir": "running_log/server_log/chess-mcp-logs/traces", + "image_dir": "running_log/server_log/chess-mcp-logs/images", + "debug": False +} + + +def _init_device_info(device_id: str = None): + """初始化设备信息""" + global _current_device_id, _device_wm_size + if device_id is None: + devices = list_devices() + if not devices: + raise RuntimeError("未连接设备") + device_id = devices[0] + + open_screen(device_id) + _current_device_id = device_id + _device_wm_size = get_device_wm_size(device_id) + return {"device_id": device_id, "device_wm_size": _device_wm_size} + + +def _get_local_server(): + """获取 LocalServer 实例""" + global _local_server + if _local_server is None: + from copilot_agent_server.local_server import LocalServer + _local_server = LocalServer(SERVER_CONFIG) + return _local_server + + +def _analyze_board(device_info) -> dict: + """截图并用视觉模型解析棋盘""" + image_path = capture_screenshot(device_info['device_id'], "tmp_screenshot", print_command=False) + image_b64_url = make_b64_url(image_path) + smart_remove(image_path) + + messages = [{ + "role": "user", + "content": [ + {"type": "image_url", "image_url": {"url": image_b64_url}}, + {"type": "text", "text": BOARD_ANALYSIS_PROMPT} + ] + }] + + response = ask_llm_anything( + model_provider=VISION_MODEL_CONFIG["model_provider"], + model_name=VISION_MODEL_CONFIG["model_name"], + messages=messages, + args={"max_tokens": 2048, "temperature": 0.1} + ) + + try: + if "" in response: + response = response.split("")[-1].strip() + + json_match = re.search(r'\{[\s\S]*\}', response) + if json_match: + data = json.loads(json_match.group()) + if "pieces" in data: + return data + except Exception as e: + print(f"解析失败: {e}") + + return {"error": "解析失败", "raw": response} + + +def _render_board(data: dict) -> str: + """渲染可视化棋盘""" + if "error" in data: + return f"解析失败: {data.get('raw', '')}" + + # 初始化空棋盘 (10行 x 9列) + board = [[' · ' for _ in range(9)] for _ in range(10)] + + # 棋子符号映射 + piece_symbols = { + # 红方(大写) + 'K': '帅', 'A': '仕', 'B': '相', 'N': '马', 'R': '车', 'C': '炮', 'P': '兵', + # 黑方(小写) + 'k': '将', 'a': '士', 'b': '象', 'n': '馬', 'r': '車', 'c': '砲', 'p': '卒', + } + + # 列名到索引 + col_map = {'A': 0, 'B': 1, 'C': 2, 'D': 3, 'E': 4, 'F': 5, 'G': 6, 'H': 7, 'I': 8} + + # 放置棋子 + pieces_list = data.get("pieces", []) + for piece in pieces_list: + pos = piece.get("pos", "") + ptype = piece.get("type", "") + if len(pos) >= 2 and pos[0] in col_map: + col = col_map[pos[0]] + try: + row = int(pos[1]) + if 0 <= row <= 9 and ptype in piece_symbols: + symbol = piece_symbols[ptype] + # 红方用 [] 包围,黑方用 () 包围 + if ptype.isupper(): + board[row][col] = f'[{symbol}]' + else: + board[row][col] = f'({symbol})' + except ValueError: + pass + + # 渲染棋盘(从上到下:9到0) + lines = [ + "", + "=" * 50, + " 当前棋盘 [红方] vs (黑方) 你执红棋", + "=" * 50, + "", + " A B C D E F G H I", + " ┌───┬───┬───┬───┬───┬───┬───┬───┬───┐" + ] + + for row in range(9, -1, -1): # 从9到0 + row_str = f"{row} │" + for col in range(9): + row_str += board[row][col] + "│" + lines.append(row_str) + + if row == 5: # 楚河汉界 + lines.append(" ├───┴───┴───┴───┴───┴───┴───┴───┴───┤") + lines.append(" │ 楚 河 汉 界 │") + lines.append(" ├───┬───┬───┬───┬───┬───┬───┬───┬───┤") + elif row > 0: + lines.append(" ├───┼───┼───┼───┼───┼───┼───┼───┼───┤") + + lines.append(" └───┴───┴───┴───┴───┴───┴───┴───┴───┘") + lines.append("") + lines.append("坐标说明:列 A-I(左到右),行 0-9(下到上)") + lines.append("红方底线=0行,黑方底线=9行,红方九宫=D0-F2,黑方九宫=D7-F9") + lines.append("=" * 50) + + return "\n".join(lines) + + +def _get_pieces_summary(data: dict) -> str: + """获取棋子列表摘要""" + if "error" in data: + return "" + + red_pieces = [] + black_pieces = [] + + for piece in data.get("pieces", []): + ptype = piece.get("type", "") + pos = piece.get("pos", "") + name = piece.get("name", "") + + if ptype.isupper(): + red_pieces.append(f"{name}({pos})") + else: + black_pieces.append(f"{name}({pos})") + + lines = [ + "", + "【红方棋子】" + "、".join(red_pieces), + "【黑方棋子】" + "、".join(black_pieces), + "" + ] + return "\n".join(lines) + + +def _pos_to_screen_desc(pos: str) -> str: + """将坐标转换为屏幕位置描述""" + if len(pos) < 2: + return "" + + col = pos[0].upper() + try: + row = int(pos[1]) + except ValueError: + return "" + + # 列的描述(A-I 对应屏幕左到右) + col_desc = { + 'A': '最左边', 'B': '左边偏左', 'C': '左边', + 'D': '中间偏左', 'E': '正中间', 'F': '中间偏右', + 'G': '右边', 'H': '右边偏右', 'I': '最右边' + }.get(col, '') + + # 行的描述(0-9 对应屏幕下到上) + if row <= 1: + row_desc = '最下方(红方底线附近)' + elif row <= 3: + row_desc = '下方(红方区域)' + elif row <= 5: + row_desc = '中间(楚河汉界附近)' + elif row <= 7: + row_desc = '上方(黑方区域)' + else: + row_desc = '最上方(黑方底线附近)' + + return f"屏幕{col_desc}、{row_desc}" + + +def _generate_screen_position_hint(instruction: str) -> str: + """从走棋指令中提取坐标并生成屏幕位置提示""" + import re + + # 匹配坐标模式:字母+数字,如 F5, C5, E0 + positions = re.findall(r'[A-Ia-i][0-9]', instruction) + + if len(positions) >= 2: + from_pos = positions[0].upper() + to_pos = positions[1].upper() + + from_desc = _pos_to_screen_desc(from_pos) + to_desc = _pos_to_screen_desc(to_pos) + + return f""" +【屏幕位置提示】 +- 起始位置 {from_pos}:{from_desc} +- 目标位置 {to_pos}:{to_desc} +""" + elif len(positions) == 1: + pos = positions[0].upper() + desc = _pos_to_screen_desc(pos) + return f"\n【屏幕位置提示】位置 {pos}:{desc}\n" + + return "" + + +# ============== MCP 工具 ============== + +@mcp.tool +def prepare_game( + task: Annotated[str, Field(description="导航任务,如'打开象棋APP,进入残局第一关'")], + device_id: Annotated[str | None, Field(description="设备ID")] = None, +) -> dict: + """ + 让 GeLab 导航到棋盘界面。看到棋盘后停止,不会下棋。 + """ + try: + device_info = _init_device_info(device_id) + server = _get_local_server() + + nav_task = f"""{task} + +【重要】你只负责导航到棋盘界面: +1. 打开象棋APP +2. 按要求进入对应模式/关卡 +3. 当你看到棋盘界面(显示红黑棋子)时,立即 COMPLETE +4. 绝对不要点击任何棋子!不要下棋!""" + + print(f"GeLab 导航: {task}") + result = evaluate_task_on_device( + agent_server=server, + device_info=device_info, + task=nav_task, + rollout_config=GELAB_CONFIG, + reflush_app=False, + reset_environment=True + ) + + success = result.get("stop_reason") == "COMPLETE" + return { + "success": success, + "message": f"导航{'成功' if success else '失败'}", + "next_step": "请调用 get_board_state() 获取棋盘状态" + } + except Exception as e: + return {"success": False, "error": str(e)} + + +@mcp.tool +def get_board_state( + device_id: Annotated[str | None, Field(description="设备ID")] = None, +) -> dict: + """ + 获取当前棋盘状态,返回可视化棋盘图。 + + ⚠️ 重要:收到棋盘后,请先进行深度思考分析,不要立即调用执行工具! + + 请按以下格式分析: + 1. 【棋局分析】列出双方棋子和局势 + 2. 【绝杀检查】检查各种绝杀可能 + 3. 【可选走法】列出几个候选走法 + 4. 【最终决定】明确写出"我选择:xxx" + + 只有写出明确结论后,才能调用 execute_chess_move() + """ + global _last_board_state + + try: + device_info = _init_device_info(device_id) + + print("解析棋盘...") + data = _analyze_board(device_info) + + if data.get("game_status") == "victory": + return { + "success": True, + "game_over": True, + "victory": True, + "message": "🎉 恭喜获胜!游戏结束。" + } + + if data.get("game_status") == "defeat": + return { + "success": True, + "game_over": True, + "victory": False, + "message": "游戏失败。" + } + + _last_board_state = data + board_visual = _render_board(data) + pieces_summary = _get_pieces_summary(data) + + return { + "success": True, + "game_over": False, + "board": board_visual, + "pieces": pieces_summary, + "raw_data": data, + "thinking_guide": f""" +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +⚠️ 请先深度思考,不要立即调用工具! + +请按以下格式分析后再决定走法: + +【棋局分析】 +- 红方棋子:... +- 黑方棋子:... +- 局势判断:... + +【绝杀检查】 +{CHECKMATE_RULES} + +【可选走法】 +1. ... +2. ... +3. ... + +【最终决定】 +我选择:[写出具体走法,如"把E0的帅移动到E1"] +理由:... + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +只有写出"我选择:xxx"后,才能调用 execute_chess_move() +""" + } + except Exception as e: + return {"success": False, "error": str(e)} + + +@mcp.tool +def execute_chess_move( + move_instruction: Annotated[str, Field(description="走棋指令,如'把F5的红车移动到C5'")], + device_id: Annotated[str | None, Field(description="设备ID")] = None, +) -> dict: + """ + 让 GeLab 执行一步走棋。 + + ⚠️ 调用前请确保你已经: + 1. 完成了深度思考分析 + 2. 明确写出了"我选择:xxx" + + 指令格式: + - 坐标格式:"把 F5 的红车移动到 C5" + - 自然语言:"红车从右边平移到左边" + """ + try: + device_info = _init_device_info(device_id) + server = _get_local_server() + + # 解析坐标并生成屏幕位置描述 + screen_hint = _generate_screen_position_hint(move_instruction) + + gelab_task = f"""象棋走棋 - 必须完成两次点击! + +【任务】{move_instruction} +{screen_hint} + +【坐标说明】 +- 列 A-I = 屏幕左到右 +- 行 0-9 = 屏幕下到上(0=红方底线,9=黑方底线) + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +⚠️ 这个任务需要点击两次才能完成! +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +【第一次点击】选中棋子 +→ 点击要移动的棋子 +→ 棋子会高亮,出现绿色可走位置提示 +→ 此时任务还没完成!必须继续! + +【第二次点击】落子 +→ 点击目标位置(绿色提示点) +→ 棋子移动到新位置 +→ 现在才算完成! + +【完成标准】 +❌ 只看到棋子高亮 = 没完成,继续点击目标! +✅ 棋子已经移动到新位置 = 完成,COMPLETE + +【特别注意】 +- 第一次点击后如果看到绿色提示点,说明选中成功 +- 此时必须立即进行第二次点击! +- 不要在选中后就停止! +- 只有棋子真正移动了才算完成!""" + + move_config = GELAB_CONFIG.copy() + move_config["max_steps"] = 10 # 增加更多步数余量 + + print(f"GeLab 执行: {move_instruction}") + result = evaluate_task_on_device( + agent_server=server, + device_info=device_info, + task=gelab_task, + rollout_config=move_config, + reflush_app=False, + reset_environment=False + ) + + success = result.get("stop_reason") == "COMPLETE" + + if success: + print("等待对手...") + time.sleep(2) + + print("获取新棋盘...") + new_state = _analyze_board(device_info) + + if new_state.get("game_status") == "victory": + return { + "success": True, + "game_over": True, + "victory": True, + "message": "🎉 恭喜获胜!" + } + + board_visual = _render_board(new_state) + pieces_summary = _get_pieces_summary(new_state) + global _last_board_state + _last_board_state = new_state + + return { + "success": True, + "game_over": False, + "move_executed": True, + "new_board": board_visual, + "pieces": pieces_summary, + "raw_data": new_state, + "next_step": "请对新棋盘进行深度思考分析,然后决定下一步" + } + else: + return { + "success": False, + "message": f"走棋失败: {result.get('stop_reason', '')}", + "hint": "请检查指令是否清晰,或调用 get_board_state() 重新确认" + } + except Exception as e: + return {"success": False, "error": str(e)} + + +@mcp.tool +def reset_session() -> dict: + """重置会话""" + global _local_server, _last_board_state + _local_server = None + _last_board_state = None + return {"success": True, "message": "已重置"} + + +if __name__ == "__main__": + import yaml + config_path = os.path.join(project_root, "mcp_server_config.yaml") + port = 8705 + if os.path.exists(config_path): + with open(config_path, "r") as f: + config = yaml.safe_load(f) + port = config.get('server_config', {}).get('chess_mcp_port', port) + + print(f"中国象棋 MCP 服务器启动在端口: {port}") + mcp.run(transport="http", port=port)