未命名笔记

生成功能,在弹出了系统消息横幅时,直接按下 F2 这个按键,产生类似鼠标点击这个消息横幅的效果。

实际就是一个很懒人的操作,用按下按键的动作,省去移动鼠标、点击的操作。

不用知道为什么,可以直接把下面中的markdown代码保存成一个md格式的文件,让后扔给Claude code,让他按照这个文档中的步骤去生成、安装。

如果不放心,可以让Claude code先解释一下这个md要做的事、要执行的操作等等。

<br />
markdown
# macOS 热键点击通知横幅 —— 完整配置教程

> 适用系统:macOS 26 (Tahoe) / 15 (Sequoia) / 14 (Sonoma)  
> 适用场景:按下 Option+X 自动点击屏幕右上角弹出的系统通知横幅,激活对应 App

---

## 目录

1. [原理概述](#1-原理概述)
2. [安装 skhd](#2-安装-skhd)
3. [配置 skhd 热键](#3-配置-skhd-热键)
4. [通知点击脚本](#4-通知点击脚本)
5. [权限设置](#5-权限设置)
6. [完整工作流程](#6-完整工作流程)
7. [调试与日志](#7-调试与日志)
8. [多屏幕适配说明](#8-多屏幕适配说明)
9. [常见问题](#9-常见问题)
10. [附录:源码](#附录源码click-notifc)
11. [附录:相关资源](#附录相关资源)

---

## 1. 原理概述

```
你按 Option+X
   │
   ▼
skhd.zig(热键守护进程)捕获按键
   │  匹配配置: alt - x : ~/click-notification/click-notif
   ▼
click-notif(C 二进制程序)启动
   │  通过 Accessibility API (AX API) 扫描 NotificationCenter 进程树
   │  条件: AXGroup, 宽300-500, 高30-200, x>1000, y<200
   ▼
找到通知横幅对应的 AXGroup 元素
   │  多条通知时选 y 最小的(最上面那条)
   ▼
AXUIElementPerformAction(el, kAXPressAction)
   │  直接通过 Accessibility API 发送"按下"动作
   ▼
通知被激活 → 对应 App 打开
   │  鼠标自动回到原位
```

**涉及的工具:**

| 工具 | 作用 | 安装方式 |
|------|------|---------|
| **skhd.zig** | 热键守护进程,捕获按键并执行命令 | `brew install jackielii/tap/skhd-zig` |
| **click-notif** | C 编写的通知点击程序 | 编译自源码(见附录) |

**为什么不直接用 AppleScript 的 `click at` 或 CGEventPost 模拟鼠标点击?**

macOS Tahoe+ 的 NotificationCenter 使用系统覆盖层窗口(AXSystemDialog),合成鼠标事件(`CGEventPost(kCGHIDEventTap)`)无法正确路由到这个层级,点击会穿透到下方应用。正确的做法是通过 Accessibility API 直接发送 `AXPress` 动作,绕过坐标映射直接与通知元素交互。

---

## 2. 安装 skhd

### 2.1 Homebrew 安装(推荐)

```bash
brew install jackielii/tap/skhd-zig
```

### 2.2 注册服务

```bash
skhd --install-service
```

这条命令会:
- 注册 LaunchAgent 实现开机自启
- 弹出 Accessibility 权限请求(需手动授权)
- 弹出 Input Monitoring 权限请求(需手动授权)

### 2.3 常用命令

```bash
skhd --start-service     # 启动服务
skhd --stop-service      # 停止服务
skhd --restart-service   # 重启服务
skhd --status            # 查看运行状态
skhd -o                  # 观察模式(回显所有按键)
skhd -k "alt-x"             # 合成按键测试
skhd -r                  # 重新加载配置
```

### 2.4 验证安装

```bash
skhd --version
skhd --status
```

如果显示 `Daemon running: Yes` 和 `Hotkeys functional: Yes` 表示安装成功。

---

## 3. 配置 skhd 热键

### 3.1 配置文件位置

skhd 按以下顺序查找配置:

1. `-c` 参数指定路径
2. `~/.config/skhd/skhdrc`
3. `~/.skhdrc`

### 3.2 创建配置

```bash
mkdir -p ~/.config/skhd
```

编辑 `~/.config/skhd/skhdrc`:

```bash
# Option+X 点击通知横幅
alt - x : osascript /Users/你的用户名/Documents/click-notification.scpt
```

### 3.3 语法说明

**基本格式:** `修饰符 - 键 : 命令`

```bash
# 普通快捷键
cmd - a : echo "Cmd+A"
ctrl + shift - t : open -a Terminal

# 无修饰符
alt - x : ~/click-notification/click-notif
f1 : open -a Safari

# Alt 修饰符说明
# skhd 中用 `alt - x`,alt 是 Option 键的别名
# 虽然 skhd -o 观察模式会显示为 Alt x,但匹配时按 `alt - x` 即可
```

**支持的修饰符:**

| 修饰符 | 说明 |
|--------|------|
| `cmd` | Command 键 |
| `ctrl` | Control 键 |
| `alt` | Option 键 |
| `shift` | Shift 键 |
| `fn` | Function 键 |
| `lcmd` / `rcmd` | 左/右 Command |
| `lctrl` / `rctrl` | 左/右 Control |
| `hyper` | cmd+shift+alt+ctrl |

**特别说明:** `alt` 在 skhd 中对应键盘上的 Option(⌥)键。写法是 `alt - x`,表示"按住 Option 键同时按 X"。

### 3.4 重新加载配置

修改配置后执行:

```bash
skhd -r
# 或
skhd --restart-service
```

---

## 4. 通知点击脚本

教程提供两种方案:

| 方案 | 性能 | 优点 | 适用场景 |
|------|------|------|---------|
| **C 二进制(推荐)** | ~0.02-0.07s | 极快,可预测性高 | 对延迟敏感的日常使用 |
| **AppleScript(备选)** | ~0.1-0.2s | 无需编译,修改方便 | 调试、快速原型 |

### 4.1 C 二进制方案(推荐)

编译好的二进制放在 `~/click-notification/click-notif`,源码及安装指南见本文件末尾附录。

### 4.2 AppleScript 方案(备选)

如果不想编译 C 程序,也可以使用 AppleScript 通过 Accessibility API 点击通知:

保存为 `~/Documents/click-notification.scpt`:

```applescript
-- 通知点击脚本(AppleScript 版)
-- 通过 Accessibility API 查找通知并执行 AXPress
-- 日志: ~/Documents/skhd-click.log

property logFile : "/Users/你的用户名/Documents/skhd-click.log"
property maxLogEntries : 30

on writeLog(msg)
    set timestamp to do shell script "date '+%Y-%m-%d %H:%M:%S'"
    try
        do shell script "echo '" & timestamp & " " & msg & "' >> " & logFile
    end try
    try
        set lineCount to do shell script "wc -l < " & logFile
        if lineCount as integer > maxLogEntries then
            do shell script "sed -i '' '1," & (lineCount - maxLogEntries) & "d' " & logFile
        end if
    end try
end writeLog

on clickNotification()
    tell application "System Events"
        tell process "NotificationCenter"
            if (count of windows) = 0 then return "no_window"

            set l1 to every UI element of window 1
            set l2 to every UI element of item 1 of l1
            repeat with panel in l2
                set panelKids to every UI element of panel
                repeat with pk in panelKids
                    if role of pk is "AXScrollArea" then
                        set notifs to every UI element of pk
                        if (count of notifs) = 0 then return "no_notif"

                        -- 取第一条通知(最上面)
                        set n to item 1 of notifs
                        set nPos to position of n
                        set nSize to size of n

                        -- 使用 AXPress 而非 click at(合成鼠标点击会穿透系统覆盖层)
                        perform action "AXPress" of n

                        set cx to (item 1 of nPos) + (item 1 of nSize) / 2
                        set cy to (item 2 of nPos) + (item 2 of nSize) / 2
                        return "AXPress 通知 (" & item 1 of nPos & "," & item 2 of nPos & ") " & ¬
                            item 1 of nSize & "x" & item 2 of nSize & " → 中心 (" & cx & "," & cy & ")"
                    end if
                end repeat
            end repeat
            return "no_match"
        end tell
    end tell
end clickNotification

on run
    set result to clickNotification()
    writeLog(result)
end run
```

skhd 配置对应改为:

```bash
alt - x : osascript ~/Documents/click-notification.scpt
```

**注意:** AppleScript 版同样使用 AXPress 而非 `click at`,原因同第 1 节所述——合成鼠标点击无法路由到系统通知覆盖层。

---

## 5. 权限设置

### 5.1 首次安装

执行 `skhd --install-service` 后,macOS 会弹出权限请求窗口。需要授予:

1. **辅助功能(Accessibility)**:skhd 需要此权限来捕获全局按键
   - 系统设置 → 隐私与安全性 → 辅助功能 → 开启 skhd

2. **输入监控(Input Monitoring)**:skhd 需要此权限来接收键盘事件
   - 系统设置 → 隐私与安全性 → 输入监控 → 开启 skhd

### 5.2 权限失效处理

```bash
# 重置权限
tccutil reset Accessibility com.jackielii.skhd
tccutil reset ListenEvent com.jackielii.skhd

# 重新启动服务(会弹出权限请求)
skhd --install-service
```

权限过期通常发生在 `brew upgrade` 后(二进制文件变更,macOS 认为 cdHash 改变)。

### 5.3 验证权限

```bash
skhd --status
```

如果显示 `Hotkeys functional: Yes` 表示权限正常。如果一直显示 `Unknown` 或 `No`,说明权限未授予。

---

## 6. 完整工作流程

### 6.1 C 二进制方案流程

#### 正常流程(有通知时)

```
通知横幅出现在屏幕右上角
        │
用户按下 Option+X
        │
   ┌────┴────┐
   │ skhd    │ 通过 Event Tap 捕获按键
   │         │ 匹配配置: alt - x → ~/click-notification/click-notif
   └────┬────┘
        │
   ┌────┴──────────────────────────┐
   │ click-notif(C 二进制)        │
   │ 1. 保存当前鼠标位置           │
   │ 2. 通过 AX API 扫描            │
   │    NotificationCenter 进程树   │
   │ 3. 定位到 AXGroup 通知元素     │
   │ 4. AXUIElementPerformAction    │
   │    (el, kAXPressAction)        │
   │    → 通知被激活,对应 App 打开 │
   │ 5. 鼠标归位                   │
   │ 6. 写入日志                   │
   └───────────────────────────────┘
```

#### 无通知时

```
用户按下 Option+X
   │
   ├─ AX API 找不到匹配元素 → 写入日志 "无通知"
   │
   └─ 程序退出,鼠标位置不变
```

### 6.2 性能评估

#### C 二进制方案

| 阶段 | 耗时 | 说明 |
|------|------|------|
| skhd 捕获 + 匹配 | ~0-2ms | 内存操作,极快 |
| C 程序启动 | ~1-3ms | 编译成的二进制无运行时开销 |
| AX API 定位元素 | ~5-40ms | 递归扫描 + 属性检查 |
| AXPress 执行 | ~1-5ms | 直接发送动作,无需坐标计算 |
| 鼠标归位 + 写日志 | ~10-30ms | 在点击之后,不影响体验 |
| **总计** | **~20-80ms** | 低于人眼感知阈值 |

#### AppleScript 方案(对比)

| 阶段 | 耗时 |
|------|------|
| osascript 进程启动 | ~30-100ms |
| AX API 操作 | ~10-50ms |
| **总计** | **~40-200ms** |

---

## 7. 调试与日志

### 7.1 查看运行日志

click-notif 在每次触发时都会写日志到 `~/Documents/skhd-click.log`:

```bash
tail -10 ~/Documents/skhd-click.log
```

日志格式示例:

```
2026-06-15 18:03:25 AXPress 通知 (1688,46) 344x57 → 中心 (1860,74) 鼠标归位 (1570,198)
2026-06-15 18:04:38 AXPress 通知 (1688,46) 344x120 → 中心 (1860,106) 鼠标归位 (1209,528)
2026-06-15 18:50:56 无通知
2026-06-15 18:51:00 AXPress 失败 err=-25200 (1688,46) 344x57 鼠标归位 (1452,306)
```

**日志字段说明:**

- `AXPress 通知 (x,y) wxh → 中心 (cx,cy) 鼠标归位 (ox,oy)` — 成功点击
- `AXPress 失败 err=<码>` — `kAXErrorCannotComplete`(-25200) 通常是通知在点击瞬间消失了
- `无通知` — AX 树中未找到匹配的通知元素

### 7.2 skhd 自身日志

```bash
cat ~/Library/Logs/skhd.log
```

### 7.3 观察模式

```bash
skhd -o
```

在观察模式下按 Option+X,会显示按键事件。注意观察输出确认 skhd 能收到按键。

### 7.4 诊断脚本

当有通知可见时,运行以下脚本可以查看通知元素在 Accessibility 层级中的精确位置:

```applescript
-- 保存为 diagnose-notification.scpt 后运行
tell application "System Events"
    tell process "NotificationCenter"
        try
            set winList to every window
            if (count of winList) = 0 then
                log "无通知窗口"
                return
            end if
            
            set l1 to every UI element of window 1
            set l2 to every UI element of item 1 of l1
            repeat with panel in l2
                set panelKids to every UI element of panel
                repeat with pk in panelKids
                    if role of pk is "AXScrollArea" then
                        set notifs to every UI element of pk
                        log "通知数量: " & (count of notifs)
                        repeat with n in notifs
                            set nPos to position of n
                            set nSize to size of n
                            log "通知: position=" & nPos & " size=" & nSize
                        end repeat
                    end if
                end repeat
            end repeat
        on error errMsg
            log "诊断失败: " & errMsg
        end try
    end tell
end tell
```

### 7.5 常见问题排查

| 症状 | 排查步骤 |
|------|---------|
| 按 Option+X 无反应 | `skhd --status` 检查是否运行;`skhd -o` 检查能否捕获按键 |
| 事件捕获正常但不触发脚本 | 检查 `~/.config/skhd/skhdrc` 配置语法;尝试 `skhd -k "alt-x"` 测试 |
| 权限错误 | `tail -20 ~/Library/Logs/skhd.log` 查看;重新授权 |
| 能点击但点到的是通知下方的窗口 | **CGEventPost 合成点击无法穿透系统覆盖层。** 改用 AXPress 方式(C 二进制已内置此修复) |
| 日志显示 AXPress 失败 | 通常是通知在点击瞬间已消失(横幅超时),可减少扫描耗时或快速按 Option+X |

---

## 8. 多屏幕适配说明

脚本通过 Accessibility API **实时获取**通知元素的位置和尺寸,天然适配不同屏幕:

- **外接显示器 / MacBook 内屏切换**:脚本每次触发时动态读取当前屏幕上的通知位置,无需改动配置
- **分辨率变化**:所有判断基于相对值(80% 高度比、30px 最小高度筛),不依赖固定像素
- **多条通知堆叠**:自动选取纵坐标最小的卡片(最上面那条),避免点击到间隙
- **通知数量变化**:单条或多条统一处理

**不需要做任何额外配置。**

---

## 9. 常见问题

### Q: `alt - x` 中的 alt 对应键盘上哪个键?

对应 Option(⌥)键。skhd 中 `alt` 是 Option 的别名,也可以写 `option`。按法是**按住 Option 再按 X**。

### Q: 换到其他电脑需要改什么?

只需要改脚本中 `logFile` 的用户名路径,以及 skhdrc 中的脚本路径。通知定位逻辑自动适配。

### Q: 点击通知后没打开 App?

确认通知是标准的 macOS 系统通知(来自 NotificationCenter)。某些 App 的自定义弹窗不是系统通知,无法通过此方式点击。

### Q: 通知有安全/隐私类的需要额外操作吗?

某些系统级通知(如屏幕录制权限请求)可能点击无效,这是 macOS 的安全机制限制。

### Q: 通知明明出现了,但按 Option+X 却点到窗口后面去了?

这是 macOS Tahoe+ 的已知行为。NotificationCenter 使用 AXSystemDialog 类型的系统覆盖层窗口,通过 `CGEventPost(kCGHIDEventTap)` 合成的鼠标点击无法路由到这个层级,导致穿透到下方应用。

**解决方法:** 使用 Accessibility API 的 `AXPress` 动作代替坐标点击。本教程的 C 二进制版和 AppleScript 版均已使用 `AXPress`,不会再出现此问题。如果使用的是旧版(用 `click at` 或 `CGEventPost` 的版本),请更新到本教程的最新源码。

### Q: click-notif 编译时需要注意什么?

```bash
cc -o ~/click-notification/click-notif /tmp/click-notif.c \
  -framework CoreGraphics \
  -framework ApplicationServices \
  -framework CoreFoundation
```

二进制会以 ad-hoc 签名运行,需要系统已授予 **skhd 辅助功能权限**(click-notif 作为 skhd 子进程继承权限)。

---

## 附录:源码(click-notif.c)

将以下代码保存为 `/tmp/click-notif.c`,然后执行:

```bash
cc -o ~/click-notification/click-notif /tmp/click-notif.c \
  -framework CoreGraphics \
  -framework ApplicationServices \
  -framework CoreFoundation
```

```c
#include <CoreGraphics/CoreGraphics.h>
#include <ApplicationServices/ApplicationServices.h>
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#include <string.h>

static char log_path[256];

static void init_log_path(void) {
    const char *home = getenv("HOME");
    if (!home) home = "/tmp";
    snprintf(log_path, sizeof(log_path), "%s/Documents/skhd-click.log", home);
}

static void write_log(const char *msg) {
    time_t now = time(NULL);
    struct tm *tm = localtime(&now);
    char ts[64];
    strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", tm);
    FILE *f = fopen(log_path, "a");
    if (f) { fprintf(f, "%s %s\n", ts, msg); fclose(f); }

    // 限制日志行数:超过 200 行时保留末尾 100 行
    char cmd[512];
    snprintf(cmd, sizeof(cmd),
        "L=$(wc -l < \"%s\"); [ \"$L\" -gt 200 ] && tail -n 100 \"%s\" > \"%s.tmp\" && mv \"%s.tmp\" \"%s\"",
        log_path, log_path, log_path, log_path, log_path);
    system(cmd);
}

static pid_t find_pid(const char *name) {
    char cmd[128];
    snprintf(cmd, sizeof(cmd), "pgrep -x %s", name);
    FILE *fp = popen(cmd, "r");
    if (!fp) return -1;
    char buf[16];
    if (!fgets(buf, sizeof(buf), fp)) { pclose(fp); return -1; }
    pclose(fp);
    return atoi(buf);
}

/* 递归扫描 AX 树,找到通知横幅。
 * 返回前会将匹配元素的 AXUIElementRef 放入 outEl(调用者 CFRelease)。
 * matchPos/matchSize 记录匹配元素的位置和尺寸(供日志用)。
 */
static int find_notification(AXUIElementRef *outEl, CGPoint *matchPos, CGSize *matchSize) {
    pid_t pid = find_pid("NotificationCenter");
    if (pid <= 0) return 0;

    AXUIElementRef app = AXUIElementCreateApplication(pid);
    if (!app) return 0;

    __block AXUIElementRef bestEl = NULL;
    __block CGPoint bestPos = {0, 0};
    __block CGSize bestSize = {0, 0};
    __block int found = 0;

    void (^__block scan)(AXUIElementRef, int) = ^(AXUIElementRef el, int depth) {
        if (depth > 8) return;

        CFTypeRef roleRef;
        if (AXUIElementCopyAttributeValue(el, kAXRoleAttribute, &roleRef) != kAXErrorSuccess)
            goto children;
        int isGroup = CFStringCompare(roleRef, CFSTR("AXGroup"), 0) == kCFCompareEqualTo;
        CFRelease(roleRef);
        if (!isGroup) goto children;

        CFTypeRef sizeRef;
        if (AXUIElementCopyAttributeValue(el, kAXSizeAttribute, &sizeRef) != kAXErrorSuccess)
            goto children;
        CGSize size;
        if (!AXValueGetValue(sizeRef, kAXValueCGSizeType, &size)) { CFRelease(sizeRef); goto children; }
        CFRelease(sizeRef);
        if (size.width < 300 || size.width > 500 || size.height < 30 || size.height > 200)
            goto children;

        CFTypeRef posRef;
        if (AXUIElementCopyAttributeValue(el, kAXPositionAttribute, &posRef) != kAXErrorSuccess)
            goto children;
        CGPoint pos;
        if (!AXValueGetValue(posRef, kAXValueCGPointType, &pos)) { CFRelease(posRef); goto children; }
        CFRelease(posRef);
        if (pos.x >= 500 && pos.y < 200) {
            // 检查当前元素是否有 AXPress 动作(有动作的才是真正可交互的通知)
            // 无 AXPress 的元素是叠放容器、占位符等,直接跳过
            int hasPress = 0;
            CFArrayRef actions = NULL;
            if (AXUIElementCopyActionNames(el, &actions) == kAXErrorSuccess) {
                CFIndex ac = CFArrayGetCount(actions);
                for (CFIndex ai = 0; ai < ac; ai++) {
                    CFStringRef actionName = CFArrayGetValueAtIndex(actions, ai);
                    if (CFStringCompare(actionName, kAXPressAction, 0) == kCFCompareEqualTo) {
                        hasPress = 1;
                        break;
                    }
                }
                CFRelease(actions);
            }
            if (!hasPress) goto children;  // 无 AXPress 则跳过,继续往深层找

            // 有 AXPress 的元素才选为最佳匹配,取 y 最小(最上面那条)
            if (!found || pos.y < bestPos.y) {
                if (bestEl) CFRelease(bestEl);
                bestEl = el;
                CFRetain(bestEl);
                bestPos = pos;
                bestSize = size;
                found = 1;
            }
        }

    children:
        if (found && depth >= 3) return;  // 找到可点击元素后不再下钻
        if (found && depth >= 3 && bestHasPress) return;
        CFTypeRef childrenRef;
        if (AXUIElementCopyAttributeValue(el, kAXChildrenAttribute, &childrenRef) != kAXErrorSuccess)
            return;
        CFArrayRef children = childrenRef;
        CFIndex count = CFArrayGetCount(children);
        for (CFIndex i = 0; i < count; i++) {
            AXUIElementRef child = CFArrayGetValueAtIndex(children, i);
            scan(child, depth + 1);
        }
        CFRelease(childrenRef);
    };

    scan(app, 0);
    CFRelease(app);

    if (found && bestEl) {
        *outEl = bestEl;
        *matchPos = bestPos;
        *matchSize = bestSize;
        return 1;
    }
    if (bestEl) CFRelease(bestEl);
    return 0;
}

int main(void) {
    init_log_path();

    // 保存当前鼠标位置
    CGEventRef cur = CGEventCreate(NULL);
    CGPoint origPos = CGEventGetLocation(cur);
    CFRelease(cur);

    // 通过 AX API 查找通知
    AXUIElementRef notifEl = NULL;
    CGPoint matchPos;
    CGSize matchSize;

    if (!find_notification(&notifEl, &matchPos, &matchSize)) {
        write_log("无通知");
        return 0;
    }

    // 通过 AXPress 点击通知(比 CGEventPost 合成点击更可靠)
    AXError err = AXUIElementPerformAction(notifEl, kAXPressAction);
    int success = (err == kAXErrorSuccess);

    // 鼠标归位
    CGWarpMouseCursorPosition(origPos);
    CGEventRef move = CGEventCreateMouseEvent(NULL, kCGEventMouseMoved, origPos, 0);
    if (move) { CGEventPost(kCGHIDEventTap, move); CFRelease(move); }
    CFRelease(notifEl);

    // 写日志
    char logmsg[256];
    if (success) {
        int cx = (int)(matchPos.x + matchSize.width / 2);
        int cy = (int)(matchPos.y + matchSize.height / 2);
        snprintf(logmsg, sizeof(logmsg),
            "AXPress 通知 (%.0f,%.0f) %.0fx%.0f → 中心 (%d,%d) 鼠标归位 (%.0f,%.0f)",
            matchPos.x, matchPos.y, matchSize.width, matchSize.height,
            cx, cy, origPos.x, origPos.y);
    } else {
        snprintf(logmsg, sizeof(logmsg),
            "AXPress 失败 err=%d (%.0f,%.0f) %.0fx%.0f 鼠标归位 (%.0f,%.0f)",
            err, matchPos.x, matchPos.y, matchSize.width, matchSize.height,
            origPos.x, origPos.y);
    }
    write_log(logmsg);

    return success ? 0 : 1;
}
```

## 附录:相关资源

- [skhd.zig GitHub](https://github.com/jackielii/skhd.zig) — 热键守护进程
- [skhd 原版](https://github.com/koekeishiya/skhd) — skhd.zig 的兼容原版
- [yabai](https://github.com/koekeishiya/yabai) — 窗口管理器,常与 skhd 搭配使用
- [cliclick](https://github.com/BlueM/cliclick) — 命令行鼠标工具(可选替代方案)