diff --git a/pc_companion/README.md b/pc_companion/README.md
new file mode 100644
index 0000000..d6819a7
--- /dev/null
+++ b/pc_companion/README.md
@@ -0,0 +1,18 @@
+# RemoteMapper PC Companion (本地脚本扩展守护程序)
+
+通过在 ESP32 WebUI 中将遥控器按键映射为 F13 ~ F24 无冲突按键,本程序可在电脑后台以 0.0% CPU 占用 实时监听并异步执行本地自定义脚本。
+
+## 快速使用方法
+
+### 第一步:在 WebUI 中将遥控器按键配置为 F13 ~ F24
+1. 电脑浏览器打开 WebUI (http://192.168.2.179 或 http://192.168.4.1);
+2. 点击想绑定的遥控器按键(例如 菜单键、电视键、电源键 或任意按键);
+3. 在弹出的高级下拉菜单中,选择 ??? 本地自定义脚本专用键 (F13 ~ F24 无冲突)(例如 F13 或 F14 等);
+4. 点击 保存映射。
+
+### 第二步:配置你想执行的本地动作
+打开 pc_companion/config.json,根据需求修改或添加命令:
+随时修改保存 config.json,程序会自动热更新重新载入!
+
+### 第三步:启动守护进程
+双击运行 start_companion.bat 即可!
diff --git a/pc_companion/autostart_manager.py b/pc_companion/autostart_manager.py
new file mode 100644
index 0000000..b5d7e03
--- /dev/null
+++ b/pc_companion/autostart_manager.py
@@ -0,0 +1,56 @@
+锘�# -*- coding: utf-8 -*-
+import os
+import sys
+import winreg
+import subprocess
+
+REG_PATH = r"Software\Microsoft\Windows\CurrentVersion\Run"
+APP_NAME = "RemoteMapperCompanion"
+
+def get_pythonw_path():
+ python_exe = sys.executable
+ pythonw = os.path.join(os.path.dirname(python_exe), "pythonw.exe")
+ if os.path.exists(pythonw):
+ return pythonw
+ return python_exe
+
+def enable_autostart():
+ script_dir = os.path.dirname(os.path.abspath(__file__))
+ host_script = os.path.join(script_dir, "remotemapper_host.py")
+ pythonw = get_pythonw_path()
+
+ cmd = f'"{pythonw}" "{host_script}"'
+
+ # Write to HKCU Run
+ key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, REG_PATH, 0, winreg.KEY_SET_VALUE)
+ winreg.SetValueEx(key, APP_NAME, 0, winreg.REG_SZ, cmd)
+ winreg.CloseKey(key)
+
+ print("=" * 60)
+ print("[OK] 鎴愬姛璁剧疆寮�鏈鸿嚜鍚姩锛�")
+ print(f" 鑷惎鐩爣: {cmd}")
+ print(" 鍚姩妯″紡: 鍚庡彴闈欓粯鏃犲脊绐楄繍琛� (pythonw.exe)")
+ print("=" * 60)
+
+ # Launch immediately in background
+ subprocess.Popen([pythonw, host_script], cwd=script_dir)
+ print("[OK] 宸插湪鍚庡彴涓烘偍鍗冲埢鍚姩闈欓粯瀹堟姢杩涚▼锛�")
+
+def disable_autostart():
+ try:
+ key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, REG_PATH, 0, winreg.KEY_SET_VALUE)
+ winreg.DeleteValue(key, APP_NAME)
+ winreg.CloseKey(key)
+ print("=" * 60)
+ print("[OK] 宸叉垚鍔熶粠寮�鏈鸿嚜鍚姩涓Щ闄わ紒")
+ print("=" * 60)
+ except FileNotFoundError:
+ print("[INFO] 褰撳墠鏈紑鍚紑鏈鸿嚜鍚��")
+ except Exception as e:
+ print(f"[ERROR] 绉婚櫎澶辫触: {e}")
+
+if __name__ == "__main__":
+ if len(sys.argv) > 1 and sys.argv[1].lower() == "disable":
+ disable_autostart()
+ else:
+ enable_autostart()
diff --git a/pc_companion/build_exe.bat b/pc_companion/build_exe.bat
new file mode 100644
index 0000000..825d939
--- /dev/null
+++ b/pc_companion/build_exe.bat
@@ -0,0 +1,9 @@
+@echo off
+cd /d "%~dp0"
+echo 正在安装/检查 PyInstaller...
+python -m pip install pyinstaller
+echo 正在打包为无黑框独立 exe...
+pyinstaller --noconsole --onefile --name RemoteMapperHost remotemapper_host.py
+echo.
+echo [OK] 打包完成!独立免安装程序位于: dist\RemoteMapperHost.exe
+pause
diff --git a/pc_companion/config.json b/pc_companion/config.json
new file mode 100644
index 0000000..0bc7b89
--- /dev/null
+++ b/pc_companion/config.json
@@ -0,0 +1,57 @@
+{
+ "F13": {
+ "name": "鎵撳紑璁$畻鍣�",
+ "command": "calc.exe",
+ "enabled": true
+ },
+ "F14": {
+ "name": "鎵撳紑娴忚鍣ㄧ綉鍧�",
+ "command": "explorer https://gemini.google.com",
+ "enabled": true
+ },
+ "F15": {
+ "name": "鎵撳紑璁颁簨鏈�",
+ "command": "notepad.exe",
+ "enabled": true
+ },
+ "F16": {
+ "name": "杩愯鑷畾涔� Python 浠诲姟",
+ "command": "python scripts/demo_task.py",
+ "enabled": true
+ },
+ "F17": {
+ "name": "杩愯鑷畾涔夋壒澶勭悊",
+ "command": "scripts/demo_action.bat",
+ "enabled": true
+ },
+ "F19": {
+ "name": "鑷畾涔夋Ы浣� 7",
+ "command": "notepad.exe",
+ "enabled": false
+ },
+ "F20": {
+ "name": "鑷畾涔夋Ы浣� 8",
+ "command": "",
+ "enabled": false
+ },
+ "F21": {
+ "name": "鑷畾涔夋Ы浣� 9",
+ "command": "",
+ "enabled": false
+ },
+ "F22": {
+ "name": "鑷畾涔夋Ы浣� 10",
+ "command": "",
+ "enabled": false
+ },
+ "F23": {
+ "name": "鑷畾涔夋Ы浣� 11",
+ "command": "",
+ "enabled": false
+ },
+ "F24": {
+ "name": "鑷畾涔夋Ы浣� 12",
+ "command": "",
+ "enabled": false
+ }
+}
diff --git a/pc_companion/disable_autostart.bat b/pc_companion/disable_autostart.bat
new file mode 100644
index 0000000..612d715
--- /dev/null
+++ b/pc_companion/disable_autostart.bat
@@ -0,0 +1,4 @@
+@echo off
+cd /d "%~dp0"
+python autostart_manager.py disable
+pause
diff --git a/pc_companion/enable_autostart.bat b/pc_companion/enable_autostart.bat
new file mode 100644
index 0000000..c7fd922
--- /dev/null
+++ b/pc_companion/enable_autostart.bat
@@ -0,0 +1,4 @@
+@echo off
+cd /d "%~dp0"
+python autostart_manager.py enable
+pause
diff --git a/pc_companion/remotemapper.ahk b/pc_companion/remotemapper.ahk
new file mode 100644
index 0000000..a027f77
--- /dev/null
+++ b/pc_companion/remotemapper.ahk
@@ -0,0 +1,52 @@
+; ==============================================================================
+; RemoteMapper AutoHotkey Companion (鏋佽嚧瓒呰交閲� 2MB 鏂规)
+; ==============================================================================
+#NoEnv
+#SingleInstance Force
+SetWorkingDir %A_ScriptDir%
+
+; F13 閿� -> 鎵撳紑璁$畻鍣�
+F13::
+ Run, calc.exe
+ return
+
+; F14 閿� -> 鎵撳紑甯哥敤缃戝潃
+F14::
+ Run, https://gemini.google.com
+ return
+
+; F15 閿� -> 鎵撳紑璁颁簨鏈�
+F15::
+ Run, notepad.exe
+ return
+
+; F16 閿� -> 杩愯 Python 鑴氭湰
+F16::
+ Run, python scripts/demo_task.py
+ return
+
+; F17 閿� -> 杩愯鎵瑰鐞嗚剼鏈�
+F17::
+ Run, scripts/demo_action.bat
+ return
+
+; F19 ~ F24 棰勭暀鎵╁睍妲戒綅
+F19::
+ ; 鍦ㄦ娣诲姞鎮ㄧ殑鑷畾涔夊懡浠�
+ return
+
+F20::
+ ; 鍦ㄦ娣诲姞鎮ㄧ殑鑷畾涔夊懡浠�
+ return
+
+F21::
+ return
+
+F22::
+ return
+
+F23::
+ return
+
+F24::
+ return
diff --git a/pc_companion/remotemapper_host.py b/pc_companion/remotemapper_host.py
new file mode 100644
index 0000000..bb89630
--- /dev/null
+++ b/pc_companion/remotemapper_host.py
@@ -0,0 +1,245 @@
+# -*- coding: utf-8 -*-
+"""
+RemoteMapper PC Companion Daemon
+================================
+Ultra-lightweight Windows background service for RemoteMapper ESP32.
+Listens for F13-F24 (and other virtual hotkeys) using native Win32 RegisterHotKey (0.0% CPU usage).
+Executes custom local scripts (.bat, .ps1, .py, apps, URLs) asynchronously.
+"""
+
+import sys
+import os
+import json
+import time
+import ctypes
+import subprocess
+import threading
+from ctypes import wintypes
+
+# Ensure Windows environment
+if sys.platform != 'win32':
+ print("[ERROR] RemoteMapper PC Companion only supports Windows.")
+ sys.exit(1)
+
+# Win32 Constants
+WM_HOTKEY = 0x0312
+MOD_NONE = 0x0000
+MOD_ALT = 0x0001
+MOD_CONTROL = 0x0002
+MOD_SHIFT = 0x0004
+MOD_WIN = 0x0008
+MOD_NOREPEAT = 0x4000
+
+# Virtual Key Mapping for F13 - F24 and standard keys
+VK_MAP = {
+ # High F-keys (F13 - F24: standard no-conflict keys)
+ "F13": 0x7C, "F14": 0x7D, "F15": 0x7E, "F16": 0x7F,
+ "F17": 0x80, "F18": 0x81, "F19": 0x82, "F20": 0x83,
+ "F21": 0x84, "F22": 0x85, "F23": 0x86, "F24": 0x87,
+ # Standard F-keys
+ "F1": 0x70, "F2": 0x71, "F3": 0x72, "F4": 0x73,
+ "F5": 0x74, "F6": 0x75, "F7": 0x76, "F8": 0x77,
+ "F9": 0x78, "F10": 0x79, "F11": 0x7A, "F12": 0x7B,
+ # Special keys
+ "PAUSE": 0x13, "SCROLLLOCK": 0x91, "PRINTSCREEN": 0x2C,
+ "APPS": 0x5D, "SLEEP": 0x5F
+}
+
+def get_app_dir():
+ """Get absolute directory path whether running from Python script or PyInstaller EXE."""
+ if getattr(sys, 'frozen', False):
+ return os.path.dirname(os.path.abspath(sys.executable))
+ return os.path.dirname(os.path.abspath(__file__))
+
+SCRIPT_DIR = get_app_dir()
+CONFIG_FILE = os.path.join(SCRIPT_DIR, "config.json")
+
+user32 = ctypes.windll.user32
+kernel32 = ctypes.windll.kernel32
+
+class RemoteMapperDaemon:
+ def __init__(self, config_path=CONFIG_FILE):
+ self.config_path = config_path
+ self.config = {}
+ self.hotkey_map = {} # id -> (key_name, config_entry)
+ self.registered_ids = []
+ self.last_config_mtime = 0
+ self.running = True
+
+ def load_config(self):
+ """Load JSON configuration mapping hotkeys to local script actions."""
+ if not os.path.exists(self.config_path):
+ self.create_default_config()
+
+ try:
+ with open(self.config_path, "r", encoding="utf-8") as f:
+ self.config = json.load(f)
+ self.last_config_mtime = os.path.getmtime(self.config_path)
+ print(f"[CONFIG] Loaded {len(self.config)} hotkey action(s) from {os.path.basename(self.config_path)}")
+ return True
+ except Exception as e:
+ print(f"[ERROR] Failed to load config.json: {e}")
+ return False
+
+ def create_default_config(self):
+ """Create sample config.json if missing."""
+ default_cfg = {
+ "F13": {
+ "name": "鎵撳紑璁$畻鍣�",
+ "command": "calc.exe",
+ "enabled": True
+ },
+ "F14": {
+ "name": "鎵撳紑娴忚鍣ㄧ綉鍧�",
+ "command": "explorer https://gemini.google.com",
+ "enabled": True
+ },
+ "F15": {
+ "name": "鎵撳紑璁颁簨鏈�",
+ "command": "notepad.exe",
+ "enabled": True
+ },
+ "F16": {
+ "name": "杩愯鑷畾涔� Python 浠诲姟",
+ "command": "python scripts/demo_task.py",
+ "enabled": True
+ },
+ "F17": {
+ "name": "杩愯鑷畾涔夋壒澶勭悊",
+ "command": "scripts/demo_action.bat",
+ "enabled": True
+ }
+ }
+ with open(self.config_path, "w", encoding="utf-8") as f:
+ json.dump(default_cfg, f, ensure_ascii=False, indent=4)
+ print(f"[INIT] Created default template: {self.config_path}")
+
+ def register_all_hotkeys(self):
+ """Register all enabled hotkeys with Windows OS."""
+ self.unregister_all_hotkeys()
+ self.hotkey_map.clear()
+ hotkey_id = 100
+
+ for key_str, entry in self.config.items():
+ if not entry.get("enabled", True):
+ continue
+
+ parts = [p.strip().upper() for p in key_str.split("+")]
+ main_key = parts[-1]
+ vk = VK_MAP.get(main_key)
+
+ if not vk:
+ print(f"[WARN] Unknown key: '{main_key}' in '{key_str}' (skipped)")
+ continue
+
+ mod = MOD_NOREPEAT
+ for p in parts[:-1]:
+ if p in ("CTRL", "CONTROL"): mod |= MOD_CONTROL
+ elif p in ("ALT", "MENU"): mod |= MOD_ALT
+ elif p in ("SHIFT",): mod |= MOD_SHIFT
+ elif p in ("WIN", "WINDOWS", "SUPER"): mod |= MOD_WIN
+
+ res = user32.RegisterHotKey(None, hotkey_id, mod, vk)
+ if res:
+ self.hotkey_map[hotkey_id] = (key_str, entry)
+ self.registered_ids.append(hotkey_id)
+ action_name = entry.get("name", "鏈懡鍚嶅姩浣�")
+ print(f" [+] 鎴愬姛鐩戝惉: {key_str: <10} -> {action_name}")
+ hotkey_id += 1
+ else:
+ err = kernel32.GetLastError()
+ print(f" [-] 娉ㄥ唽澶辫触: {key_str} (鍙兘宸茶鍏朵粬绋嬪簭鍗犵敤, Win32 Error: {err})")
+
+ def unregister_all_hotkeys(self):
+ """Unregister previously registered hotkeys."""
+ for hid in self.registered_ids:
+ user32.UnregisterHotKey(None, hid)
+ self.registered_ids.clear()
+
+ def execute_action(self, key_str, entry):
+ """Execute the configured command asynchronously with robust relative path and Python environment resolution."""
+ name = entry.get("name", "鏈懡鍚嶅姩浣�")
+ cmd = entry.get("command", "").strip()
+
+ if not cmd:
+ print(f"[TRIGGER] {key_str} 鎸変笅 -> 鏃犻厤缃懡浠�")
+ return
+
+ # 1. Expand {DIR} or {ROOT} placeholders
+ cmd = cmd.replace("{DIR}", SCRIPT_DIR).replace("{ROOT}", SCRIPT_DIR)
+
+ # 2. Smart Python path resolution (works even if Python is not in system PATH on end-user PC)
+ if cmd.startswith("python ") or cmd.startswith("pythonw "):
+ py_bin = sys.executable
+ if cmd.startswith("pythonw "):
+ py_bin = py_bin.replace("python.exe", "pythonw.exe")
+ cmd = f'"{py_bin}" ' + cmd.split(" ", 1)[1]
+
+ print(f"\n[TRIGGER] >>> 閬ユ帶鍣ㄦ寜涓� {key_str} [{name}] -> 鎵ц鍛戒护: {cmd}")
+
+ def _run():
+ try:
+ # Force working directory to SCRIPT_DIR so all relative paths (scripts/...) work anywhere
+ subprocess.Popen(cmd, shell=True, cwd=SCRIPT_DIR)
+ except Exception as ex:
+ print(f"[ERROR] 鎵ц澶辫触 [{cmd}]: {ex}")
+
+ threading.Thread(target=_run, daemon=True).start()
+
+ def check_config_reload(self):
+ """Periodically check if config.json was edited by user and hot-reload."""
+ try:
+ if os.path.exists(self.config_path):
+ mtime = os.path.getmtime(self.config_path)
+ if mtime > self.last_config_mtime:
+ print("\n[HOT-RELOAD] 妫�娴嬪埌 config.json 宸蹭慨鏀癸紝姝e湪閲嶆柊杞藉叆閰嶇疆...")
+ if self.load_config():
+ self.register_all_hotkeys()
+ except Exception:
+ pass
+
+ def run(self):
+ """Main message loop with Windows event pump (0% CPU)."""
+ print("=" * 60)
+ print(" RemoteMapper PC Companion - 鏋佺畝瓒呰交閲忔湰鍦拌剼鏈墿灞曞畧鎶よ繘绋�")
+ print(f" 宸ヤ綔鐩綍: {SCRIPT_DIR}")
+ print("=" * 60)
+
+ self.load_config()
+ self.register_all_hotkeys()
+
+ print("\n[READY] 姝e湪鍚庡彴鐩戝惉閬ユ帶鍣ㄥ揩鎹烽敭 (0.0% CPU 鏋佷綆寮�閿�)...")
+ print("[TIP] 闅忔椂缂栬緫 config.json 鍗冲彲瀹炴椂鐑洿鏂帮紝鎸� Ctrl+C 閫�鍑虹▼搴忋�俓n")
+
+ msg = wintypes.MSG()
+ last_check_time = time.time()
+
+ while self.running:
+ # Check for config reload every 1.5s
+ now = time.time()
+ if now - last_check_time > 1.5:
+ self.check_config_reload()
+ last_check_time = now
+
+ # Process any available messages without busy looping
+ while user32.PeekMessageW(ctypes.byref(msg), None, 0, 0, 1): # PM_REMOVE = 1
+ if msg.message == WM_HOTKEY:
+ hid = msg.wParam
+ if hid in self.hotkey_map:
+ key_str, entry = self.hotkey_map[hid]
+ self.execute_action(key_str, entry)
+ user32.TranslateMessage(ctypes.byref(msg))
+ user32.DispatchMessageW(ctypes.byref(msg))
+
+ # Sleep 30ms to prevent CPU spin
+ time.sleep(0.03)
+
+ self.unregister_all_hotkeys()
+
+if __name__ == "__main__":
+ daemon = RemoteMapperDaemon()
+ try:
+ daemon.run()
+ except KeyboardInterrupt:
+ print("\n[EXIT] RemoteMapper PC Companion 宸查��鍑恒��")
+ daemon.unregister_all_hotkeys()
diff --git a/pc_companion/scripts/demo_action.bat b/pc_companion/scripts/demo_action.bat
new file mode 100644
index 0000000..6babbda
--- /dev/null
+++ b/pc_companion/scripts/demo_action.bat
@@ -0,0 +1,6 @@
+@echo off
+echo ===================================================
+echo [RemoteMapper] Triggered local batch script!
+echo Time: %date% %time%
+echo ===================================================
+timeout /t 3
diff --git a/pc_companion/scripts/demo_task.py b/pc_companion/scripts/demo_task.py
new file mode 100644
index 0000000..4cb00c8
--- /dev/null
+++ b/pc_companion/scripts/demo_task.py
@@ -0,0 +1,5 @@
+锘�# -*- coding: utf-8 -*-
+import datetime
+
+print(f"[{datetime.datetime.now().strftime('%H:%M:%S')}] RemoteMapper Python 鑷畾涔夎剼鏈Е鍙戞垚鍔燂紒")
+# 鎮ㄥ彲浠ュ湪杩欓噷缂栧啓浠绘剰鑷姩鍖栭�昏緫锛屼緥濡傝皟鍙栧ぇ妯″瀷 API銆佹搷鎺ц蒋浠躲�佸彂閫佺綉缁滆姹傜瓑
diff --git a/pc_companion/scripts/sample_action.ps1 b/pc_companion/scripts/sample_action.ps1
new file mode 100644
index 0000000..067bde5
--- /dev/null
+++ b/pc_companion/scripts/sample_action.ps1
@@ -0,0 +1,3 @@
+锘�# PowerShell 绀轰緥鑴氭湰
+Write-Host "[RemoteMapper] 瑙﹀彂 PowerShell 鑴氭湰" -ForegroundColor Cyan
+[System.Media.SystemSounds]::Asterisk.Play()
diff --git a/pc_companion/silent_start.vbs b/pc_companion/silent_start.vbs
new file mode 100644
index 0000000..17e9b0a
--- /dev/null
+++ b/pc_companion/silent_start.vbs
@@ -0,0 +1,2 @@
+Set WshShell = CreateObject("WScript.Shell")
+WshShell.Run "pythonw.exe "" & Replace(WScript.ScriptFullName, "silent_start.vbs", "remotemapper_host.py") & "", 0, False
diff --git a/pc_companion/start_companion.bat b/pc_companion/start_companion.bat
new file mode 100644
index 0000000..6bdc845
--- /dev/null
+++ b/pc_companion/start_companion.bat
@@ -0,0 +1,13 @@
+@echo off
+cd /d "%~dp0"
+cls
+echo ========================================================
+echo RemoteMapper PC Companion Service
+echo ========================================================
+echo.
+python remotemapper_host.py
+if errorlevel 1 (
+ echo.
+ echo [ERROR] Python script exited with error.
+ pause
+)
diff --git a/src/web/web_ui.h b/src/web/web_ui.h
index a90ffdc..51ffa29 100644
--- a/src/web/web_ui.h
+++ b/src/web/web_ui.h
@@ -434,6 +434,20 @@ const char INDEX_HTML[] PROGMEM = R"rawliteral(
+