picopaste 把 Windows 截图送到 Linux 的步骤压缩为一次热键:截图进入剪贴板后按 alt+shift+v,工具通过已有 SSH 通道上传图片,将远端绝对路径写入剪贴板并注入当前焦点窗口。Linux 端不安装新服务、不开放额外端口,也不需要工具接触 SSH 凭据。

使用方式与问题边界

典型场景:编辑器、终端、构建流程和 Claude Code 运行在 Linux 主机,而操作界面和截图工具运行在 Windows。手工执行 scp、使用共享目录或借助中转工具,都需要重新输入路径或切换注意力。单日重复十余次,累积的时间与注意力开销相当可观。

获取 release 中的 picopaste.exe,或自行构建,在其同级目录放置 picopaste.ini:

[picopaste]
host = user@your-linux-host
hotkey = alt+shift+v

运行后托盘图标会出现,连接建立期间最多短暂显示黄色,成功后变为绿色。截取图片并按热键即可上传。仓库地址是 https://github.com/DeguiLiu/picopaste,许可为 MIT。

单进程链路与远端依赖

远端使用 stock sshd 自带的 sftp-server 子系统,绝大多数 Linux 开发机默认已经开启。Windows 侧只有一个进程,不实现 SSH 协议,而是驱动系统已有的 ssh:

ssh -o ClearAllForwardings=yes -s <host> sftp

-s 请求 SFTP 子系统,stdin/stdout 随之成为 SFTP 字节流,全程不经过远端 shell。认证、密钥、ssh-agent、known_hosts 和 ~/.ssh/config 交由系统 OpenSSH 处理;工具不读取凭据,也默认不修改 ~/.ssh/config。

ClearAllForwardings=yes 用于屏蔽 SSH 配置中可能存在的端口转发,因为当前通道只需要 SFTP 子系统,不需要建立隧道。

flowchart LR
    subgraph W["Windows 客户端 · 单进程 picopaste.exe"]
        HK["全局热键 · RegisterHotKey MOD_NOREPEAT"]:::win
        ML["主线程消息循环 · GetMessageW(阻塞,无定时器)"]:::win
        TRAY["托盘 · Shell_NotifyIconW + TaskbarCreated 重挂"]:::win
        CLIP["剪贴板采集 · PNG 快路径 / DIB → WIC"]:::win
        UP["上传 worker · SFTP 状态机"]:::win
        INJ["焦点守卫 + SendInput · 核对注入事件数"]:::win
        JOB["Job Object · KILL_ON_JOB_CLOSE + MEMORY_LIMIT"]:::win
    end
    subgraph R["Linux 远端 · 零代码"]
        SSHD["sshd · sftp-server 子系统"]:::lnx
        DIR["/tmp/picopaste(默认,客户端经 SFTP 创建)"]:::lnx
        CC["Claude Code · 按路径读取图片"]:::ext
    end
    HK --> ML
    ML --> UP
    UP --> CLIP
    ML --> TRAY
    UP --> SSHD
    SSHD --> DIR
    UP --> INJ
    INJ --> CC
    CC --> DIR
    JOB -.-> SSHD
    classDef win fill:#1f3a5f,stroke:#4a90d9
    classDef lnx fill:#1f4a2f,stroke:#4aa96c
    classDef ext fill:#4a3a1f,stroke:#d9a441

用户侧始终只有一个进程,Linux 远端没有引入新组件。

SFTP 上传的原子性

scp 并不是这里的重点。OpenSSH 9.0 及以上版本的 scp 默认使用 SFTP,远端路径不再经过 shell 展开,~ 与引号的行为容易与预期不同。使用 ssh 'cat > path' 又依赖远端的 sh、mkdir、cat 和 wc,还需要自行处理引号;传输期间目标文件名已经存在半成品。

picopaste 选择在同一条连接内使用 SFTP v3。自行实现 SFTP v3 只需要 10 个报文,每一步都能获得明确的成功或失败码,并可在同一条连接内比对字节数:

sequenceDiagram
    participant P0 as 主线程
    participant P1 as 上传 worker
    participant P2 as 剪贴板
    participant P3 as SFTP 通道
    Note over P1: 连接失败则进入退避重连,由等待超时驱动,无需轮询
    Note over P1: PNG 直写临时文件;否则 DIBV5/DIB → WIC 编码(不复制像素)
    P1->>P3: 启动即建连(EnsureChannel),成功即 Ready(托盘变绿)
    P0->>P1: 热键触发,投递上传任务(不阻塞消息循环)
    P1->>P2: OpenClipboard → 取图 → CloseClipboard
    P1->>P3: REALPATH "." → MKDIR uploads → OPEN .tmp--
    P1->>P3: WRITE
    P1->>P3: CLOSE → STAT(远端字节数与本地比对,不符即中止)
    P1->>P3: RENAME → clip--.png(此前中断不留半成品)
    P1->>P2: 写入路径文本(CF_UNICODETEXT + OleFlush)
    P1->>P3: SendInput Ctrl+Shift+V(核对事件数)

只有 STAT 确认字节数一致后才执行 RENAME,因此 Claude Code 不会读到尚未传输完成的文件。