背景与原理
在 Mac 上通过 VS Code Remote SSH 连到远端无图形界面的 Fedora 开发机使用 Pi 时,按 Ctrl+V 默认读不到 Mac 本地的剪贴板图片。
解决思路很简单:
- Mac 端:通过
pngpaste 导出剪贴板图片,用 launchd 监听端口(inetd 模式按需调用); - 传输层:通过 SSH 反向隧道(
RemoteForward)把 Mac 本地的服务暴露给 Fedora 的 127.0.0.1:7779; - Fedora 端:安装
pi-ssh-image-clipboard 插件,拦截 Ctrl+V,从 127.0.0.1:7779 读取图片写入 /tmp/,并将文件路径填入 Pi 输入框。
flowchart LR
%% mermaid-hash: a75a9eae
A[Mac 剪贴板] --> B[launchd + pngpaste
127.0.0.1:7779]
B -->|SSH RemoteForward| C[Fedora 127.0.0.1:7779]
C --> D[pi-ssh-image-clipboard]
D --> E["/tmp/pi-clipboard-UUID.png"]
E --> F[Pi 输入框]整个链路不复杂,但配置时最容易踩的坑是:以为在 ~/.ssh/config 里写了 RemoteForward 就完事了,实际当前已有的 VS Code SSH 连接或 ControlMaster 并不会动态补上新转发。
1. Mac 端配置:提供图片服务
安装 pngpaste
1
2
3
4
| brew install pngpaste
which pngpaste
# Apple Silicon: /opt/homebrew/bin/pngpaste
# Intel Mac: /usr/local/bin/pngpaste
|
配置 clipserve (LaunchAgent)
新建 ~/Library/LaunchAgents/org.pasky.clipserve.plist:
注意:<string> 中的 pngpaste 路径务必替换为 which pngpaste 的实际输出。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>org.pasky.clipserve</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/pngpaste</string>
<string>-</string>
</array>
<key>Sockets</key>
<dict>
<key>Listeners</key>
<dict>
<key>SockNodeName</key>
<string>127.0.0.1</string>
<key>SockServiceName</key>
<string>7779</string>
</dict>
</dict>
<key>inetdCompatibility</key>
<dict>
<key>Wait</key>
<false/>
</dict>
<key>StandardErrorPath</key>
<string>/tmp/pngpaste-clipserve.err</string>
</dict>
</plist>
|
使用 inetd 模式的好处是:平时不占用后台进程,只有端口收到连接时才调起 pngpaste - 吐出二进制流。
加载服务并验证:
1
2
3
4
5
6
| # 启动服务
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/org.pasky.clipserve.plist
# 截图到剪贴板后测试本地能否读取
nc 127.0.0.1 7779 | file -
# 正常输出:/dev/stdin: PNG image data, ...
|
2. 传输层:常驻 SSH 反向隧道
踩坑:为什么不直接靠 VS Code SSH 转发?
VS Code Remote SSH 复用的是初始建立的 SSH 通道,在 ~/.ssh/config 新增 RemoteForward 后,不断开重连是不会生效的。而且只要 VS Code 窗口关掉或连接重置,转发就会中断。
更稳妥的方式是用单独的 LaunchAgent 维护一条专职转发的 SSH 隧道。
配置 SSH Tunnel (LaunchAgent)
新建 ~/Library/LaunchAgents/org.pasky.pi-clip-tunnel.plist:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>org.pasky.pi-clip-tunnel</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/ssh</string>
<string>-S</string>
<string>none</string>
<string>-NT</string>
<string>-o</string>
<string>BatchMode=yes</string>
<string>-o</string>
<string>ExitOnForwardFailure=yes</string>
<string>-o</string>
<string>ServerAliveInterval=30</string>
<string>-o</string>
<string>ServerAliveCountMax=3</string>
<string>-o</string>
<string>ConnectTimeout=10</string>
<string>-R</string>
<string>127.0.0.1:7779:127.0.0.1:7779</string>
<string>fedora</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>ThrottleInterval</key>
<integer>10</integer>
<key>StandardErrorPath</key>
<string>/tmp/pi-clip-tunnel.err</string>
</dict>
</plist>
|
关键参数:
-S none:不复用已有的 SSH ControlMaster,避免被其他会话状态干扰;-o ExitOnForwardFailure=yes:端口冲突或转发失败时立即退出,交给 launchd 的 KeepAlive 重试;-o BatchMode=yes:非交互模式,依赖配置好的 SSH Key(需已配好公钥免密登录或 1Password / ssh-agent)。
加载隧道:
1
| launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/org.pasky.pi-clip-tunnel.plist
|
在 Fedora 上验证隧道是否通畅:
1
2
3
4
5
| # 检查端口监听
ss -ltn '( sport = :7779 )'
# 测试读取 Mac 剪贴板
nc 127.0.0.1 7779 | file -
|
3. Fedora 端配置:安装与配置 Pi 插件
安装插件(锁定 Commit)
Pi Extension 和 Pi 运行在同一权限下,建议锁定经过审计的 commit 安装:
1
| pi install 'git:github.com/pasky/pi-ssh-image-clipboard@55db30a67dc039aec243c0737f5fec179add057c'
|
解绑内置冲突快捷键
Pi 原生自带的 app.clipboard.pasteImage 同样绑定了 Ctrl+V,在无 GUI 的 Linux 下无法工作还会截断事件。
在 Fedora 的 ~/.pi/agent/keybindings.json 中将其置空:
1
2
3
| {
"app.clipboard.pasteImage": []
}
|
修改后在 Pi 终端里执行 /reload 重新加载配置。
排障速查
Fedora 报 Connection refused
- 原因:远端 7779 端口未建立监听(转发隧道未通)。
- 排查:Fedora 上运行
ss -ltn '( sport = :7779 )' 检查端口;Mac 上测试 ssh -S none -o BatchMode=yes fedora true 确认 SSH 密钥是否支持非交互免密登录。
报 Remote forwarding failed
- 原因:远端 7779 端口被旧残余进程占用,或 sshd 禁止了端口转发。
- 排查:Fedora 上运行
ss -ltnp '( sport = :7779 )' 查看占用进程;检查远端 /etc/ssh/sshd_config 是否开启 AllowTcpForwarding yes。
提示 No image on client clipboard
- 原因:Mac 当前剪贴板中没有可识别的图片数据。
- 排查:在 Mac 本地执行
pngpaste - | file - 验证剪贴板是否有有效图像流。
按 Ctrl+V 没反应或报快捷键冲突
- 原因:Pi 内置图片粘贴动作未解绑,或插件未生效。
- 排查:检查
~/.pi/agent/keybindings.json 配置,并在 Pi 终端中执行 /reload。
发送后模型报错无法识别图片
- 原因:当前激活的模型不支持 Vision(图片)输入。
- 排查:运行
pi --list-models,确认当前模型在 images 一列是否标记为 yes。
安全与多用户注意事项
回环端口暴露:
127.0.0.1:7779 在单人开发机上最省事,但在多人共享的服务器上,其他本地用户也能连该端口偷读你的 Mac 剪贴板。多用户环境建议改用插件支持的 Unix Socket 方案:~/.pi-clip/<client>.sock(配合 chmod 700)。
临时文件权限:
插件保存的 /tmp/pi-clipboard-*.png 默认权限是 0644。如果涉及敏感业务截图,可配置别名让 Pi 在严格 umask 下运行:
1
| alias pi='(umask 077 && command pi "$@")'
|
并定期清理历史缓存:
1
| find /tmp -maxdepth 1 -user "$USER" -name 'pi-clipboard-*' -mtime +1 -delete
|