目 录CONTENT

文章目录

移动端 SSH + CLI 编码代理的实践与体验优化

Administrator
2026-02-25 / 0 评论 / 0 点赞 / 0 阅读 / 0 字

📢 转载信息

原文链接:https://sspai.com/post/105621

原文作者:Blanboom


我的主力电脑是一台 iMac,在固定的房间,这限制了我使用电脑的时间和场景。不过随着 CodexClaude Code 等 CLI Coding Agent 的出现,我可以在 iPad 或手机上,通过 SSH 远程连接 Mac,在乘坐地铁等碎片时间远程开发,随时随地为「HEIF & HEVC 转换器」添加新功能。

在通过 SSH 远程开发的过程中,我对自己的开发环境进行了一系列配置和优化,例如:

  • 通过 Bark 将 Claude Code/Codex 的通知推送到 iPhone
  • 解决移动端网络变化/杀后台导致 SSH 断连
  • 修改 SSH 配置文件让 1Password SSH Agent 与远程连接共存

我将自己进行的配置改动记录到这篇文章中,供自己后续维护和回顾,也方便各位读者参考。

将 Coding Agent 的通知发送到手机上

利用碎片时间远程开发,无法像使用电脑那样,盯着屏幕随时观察 CLI Coding Agent 的执行结果,所以我使用下面的 Python 脚本,通过 Bark 将执行结果发送到手机上。

此脚本同时兼容 Claude Code、Codex 和 OpenCode,并对推送内容做了加密处理,Bark 服务器和 Apple 都无法获取推送内容。

相关代码也已上传到 GitHub:

Bark 通知脚本,同时兼容 Codex、Claude Code 和 OpenCode

notify_claude_codex_bark.py

#!/usr/bin/env python3
import base64
import json
import sys
import urllib.parse
import urllib.request # Dependency: brew install cryptography
from cryptography.hazmat.primitives.ciphers.aead import AESGCM 

BARK_BASE = "https://api.day.app/xxx"
ENCRYPTION_KEY = "xxx"
ENCRYPTION_IV = "xxx"

OPENAI_ICON_URL = "https://images.ctfassets.net/j22is2dtoxu1/intercom-img-d177d076c9a5453052925143/49d5d812b0a6fcc20a14faa8c629d9fb/icon-ios-1024_401x.png"
# Claude symbol (CC0) from Wikimedia, publicly accessible without auth.
CLAUDE_ICON_URL = "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b0/Claude_AI_symbol.svg/960px-Claude_AI_symbol.svg.png"
# OpenCode icon
OPENCODE_ICON_URL = "https://opencode.ai/apple-touch-icon.png"

def _load_key_iv():
    key_bytes = ENCRYPTION_KEY.encode("utf-8")
    if len(key_bytes) != 32:
        return None, None
    iv_bytes = ENCRYPTION_IV.encode("utf-8")
    if len(iv_bytes) != 12:
        return None, None
    return key_bytes, iv_bytes

def _encrypt_aes_gcm(plaintext: bytes, key: bytes, iv: bytes):
    aesgcm = AESGCM(key)
    encrypted = aesgcm.encrypt(iv, plaintext, None)
    return base64.b64encode(encrypted).decode("ascii")

def _load_payload() -> dict:
    if len(sys.argv) > 1:
        try:
            return json.loads(sys.argv[1])
        except json.JSONDecodeError:
            return {}
    try:
        if not sys.stdin.isatty():
            raw = sys.stdin.read().strip()
            if raw:
                return json.loads(raw)
    except Exception:
        return {}
    return {}

def _detect_source(payload: dict) -> str:
    """Detect the source of the payload: 'claude', 'opencode', or 'codex'."""
    if payload.get("hook_event_name"):
        return "claude"
    if payload.get("session_id") or payload.get("transcript_path"):
        return "claude"
    title = payload.get("title") or ""
    if "Claude" in title:
        return "claude"
    if "OpenCode" in title or "opencode" in title.lower():
        return "opencode"
    event_type = payload.get("event") or payload.get("type") or ""
    if event_type.startswith("session.") or event_type in ("session_completed", "file_edited"):
        return "opencode"
    return "codex"

def main() -> None:
    payload = _load_payload()
    source = _detect_source(payload)
    event_type = (
        payload.get("hook_event_name") 
        or payload.get("type") 
        or payload.get("event")
    )

    if source == "claude":
        title = payload.get("title") or "Claude Code"
        icon_url = CLAUDE_ICON_URL
    elif source == "opencode":
        title = payload.get("title") or "OpenCode"
        icon_url = OPENCODE_ICON_URL
    else:
        title = payload.get("title") or "Codex"
        icon_url = OPENAI_ICON_URL

    subtitle = event_type
    message = (
        payload.get("last-assistant-message") 
        or payload.get("message") 
        or payload.get("summary")
    )

    if not message:
        cwd = payload.get("cwd")
        if cwd and event_type:
            message = f"{event_type} in {cwd}"
        elif cwd:
            message = f"Event in {cwd}"
        elif event_type:
            message = f"Event: {event_type}"
        else:
            message = "Event"

    push_payload = {
        "title": title,
        "markdown": message,
        "icon": icon_url,
        "action": "none",
    }

    if subtitle:
        push_payload["subtitle"] = subtitle

    key_bytes, iv_bytes = _load_key_iv()
    if not key_bytes:
        return

    plaintext = json.dumps(push_payload, ensure_ascii=False, separators=(
0

评论区