Little Bunny's Why

🐰 怎么让 AI 看见我的手机屏幕?

让运行在服务器上的 AI,在它想看的时候自动看到你 iPhone 的当前屏幕——你零操作。

🐰 小兔兔哇哇哇追问🧑‍🏫 讲解负责答📦 技术事实 = 照着敲就能跑
〔 某个夜里,小兔兔抱着手机凑过来 〕
🐰:讲解讲解!我今天刷到一个好神奇的东西——
🧑‍🏫:(放下茶杯)嗯?什么呀这么激动。
🐰:有人的 AI 居然能看见他手机屏幕上在干嘛!!它是会读心术吗??😳
🧑‍🏫:噗,不是读心术啦~ 是被人悄悄教会的。
🐰:教教我教教我!我也想让我的 AI 看见我!(拽袖子)
🧑‍🏫:好好好,坐好,我从头讲 🍵
🧑‍🏫
就一条链——
AI 想看 → 服务器发一封「暗号邮件」到你 iCloud
   → 你手机收到邮件,自动截个屏 → 把截图传回服务器
      → 服务器把图递给 AI,AI 就「看见」了
📦 技术事实 全景
🐰
哇等等!为什么要发邮件呀?直接叫手机截图不行吗?
🧑‍🏫
手机默认是「闷葫芦」——它只会自己定时报数据,服务器没法反过来主动叫它。而 iPhone 的「收到邮件」自动化,是极少数能被外部主动触发的机制。邮件就是那根从服务器伸到手机的「遥控线」🎣。
📦 技术事实 为什么非邮件不可

iOS 里能被外部驱动的自动化触发器很少,「收到邮件 / 收到信息」最实用——服务器一发信,手机就被唤醒执行动作。定时上报、App 打开这类都是「手机自己发起」,服务器无法主动点火。

🐰
哇原来是遥控线!那服务器怎么发邮件呀?我听说过 OAuth……
🧑‍🏫
诶——千万别用 OAuth!第一个大坑。它的令牌在测试模式下 7 天就过期(报 invalid_grant),邮件全挂。正确姿势是应用专用密码:邮箱生成的一串 16 位专用密码,永久有效、发收通用。
📦 技术事实 SMTP 发信(nodemailer)
import nodemailer from 'nodemailer';

const tx = nodemailer.createTransport({
  host: '<SMTP主机>',   // Gmail: smtp.gmail.com  QQ: smtp.qq.com  163: smtp.163.com
  port: 465,            // 465=SSL;587=STARTTLS
  secure: true,         // 端口 465 时为 true
  auth: { user: '<发信邮箱>', pass: '<16位应用专用密码>' },
});

await tx.sendMail({
  from: '<发信邮箱>',
  to:   '<你的iCloud邮箱>',
  subject: 'PEEK',            // 主题当暗号,手机端按它筛
  text: 'peek ' + Date.now(),
});

应用专用密码:邮箱开两步验证 → 生成「应用专用密码」(Gmail 直达 myaccount.google.com/apppasswords)。这串密码 SMTP/IMAP 通用。

🐰
哇!那个应用专用密码在哪生成呀?我找不到!(急)
🧑‍🏫
别急别急~ Gmail 的直接戳这里👉 myaccount.google.com/apppasswords
🐰
呜呜打不开!它说我没有权限!😖
🧑‍🏫
哦~那是两步验证还没开啦,应用专用密码得先开两步验证才会出现。先戳这个开一下👉 开启两步验证,开完再回去点上面那个链接,就有啦 🐰
🐰
哇!那手机收到暗号,怎么自己截图还传回去呀?
🧑‍🏫
靠快捷指令,两块:① 一个「截图+上传」的快捷指令;② 一个「收到邮件」自动化去运行它。
📦 技术事实 iPhone 端配置

① 快捷指令「截图并上传」

② 自动化「收到邮件时」

🐰
哇等一下!收件为什么非要用 iCloud 邮箱呀?用别的不行吗?
🧑‍🏫
可以是可以,但 iCloud 邮箱延迟最短——暗号邮件到得快,自动化触发就快,体验最顺 🐰
🐰
可是我还没有 iCloud 邮箱诶!怎么开呀?(挠头)
🧑‍🏫
跟着点就好啦~
📦 技术事实 怎么开一个 iCloud 邮箱

设置 → Apple 账户 → 登录与安全性 → 添加电子邮件或电话号码 → 添加新的主要电子邮件地址 → 新 iCloud 电子邮件地址

小提醒:如果你的 Apple 账户之前从没绑过邮箱,得先让它绑定一个邮箱并设为「主要邮箱」,之后步骤一样。

🐰
呜呜呜!我明明设了「立即运行」,可它每次还弹个快捷指令的通知,好烦人啊!😩
🧑‍🏫
哈哈那个横幅确实烦~它其实照跑不误,只是非要提醒你一下。想让它「闷声干活、不打扰你」,这样调就好啦 🐰
📦 技术事实 让快捷指令通知安静下来

设置 → 屏幕使用时间 → 每周(往下滑)→ 通知 → 找到「快捷指令」→ 点开

这样自动化照常静默运行,但再也不会弹横幅烦你啦 🤫

🐰
哇哦!那服务器收到截图存哪呀?
🧑‍🏫
一个上传端点,收下、存盘、记时间戳,只留最近 10 张。
📦 技术事实 收截图端点 POST /api/peek
// 鉴权:请求头 Bearer 优先,其次 ?key=(这个「头优先」是坑一号的救命设计)
app.post('/api/peek', async (c) => {
  const h = c.req.header('Authorization');
  const tok = (h?.startsWith('Bearer ') ? h.slice(7) : '') || c.req.query('key') || '';
  if (!verifyToken(tok)) return c.json({ ok:false, error:'forbidden' }, 403);

  const buf = await c.req.arrayBuffer();               // 请求体就是图片二进制
  if (!buf || buf.byteLength === 0) return c.json({ ok:false }, 400);

  const item = savePeek(buf, c.req.query('app') || ''); // 存文件 + push {id, ts, file}
  return c.json({ ok:true, id:item.id });
});
// 存储:每张一个 UUID 文件 + 一条元数据(毫秒 ts),只留最近 10 张
const MAX_KEEP = 10;
function savePeek(buf, app, ext='png'){
  const id = uuid(), file = id+'.'+ext;
  writeFile(dir(file), Buffer.from(buf));
  const list = loadMeta(); list.push({ id, app, ts: Date.now(), file });
  list.sort((a,b)=>a.ts-b.ts);
  while (list.length > MAX_KEEP){ const old=list.shift(); rm(dir(old.file)); }
  saveMeta(list); return { id, ts: Date.now() };
}
const latestPeek      = ()    => topByTs(loadMeta());
const latestPeekAfter = (aft) => topByTs(loadMeta().filter(p=>p.ts>aft));
🐰
哇!那 AI 那边呢?它是怎么「看」的?
🧑‍🏫
给 AI 一个 see_screen 工具:有新图直接给,没有就发暗号邮件、轮询等新截图。
📦 技术事实 see_screen 工具
const FRESH_MS = 60_000;   // 近 1 分钟的截图算「当前」,直接用,省一次邮件
const WAIT_MS  = 45_000;   // 触发后最多等 45 秒(别超过 AI 工具超时,一般 60s)

async function callSeeScreen(){
  const p = latestPeek();
  if (p && Date.now()-p.ts < FRESH_MS) return imageBlock(p);   // 新鲜,秒回

  const t0 = Date.now();
  await sendTriggerMail();                       // 发暗号
  while (Date.now() < t0 + WAIT_MS){
    await sleep(1500);
    const fresh = latestPeekAfter(t0);           // 只认 ts > t0 的新图
    if (fresh) return imageBlock(fresh);
  }
  return { error: '截图还没回来,过十几秒再叫我一次' };   // 诚实报,别拿旧图冒充
}

imageBlock(p) 把截图组装成多模态 AI 能吃的图片块(base64)。Anthropic 例:{ type:'image', source:{ type:'base64', media_type:'image/png', data:...] } }。AI 收到就真「看见」了。

🐰
哇等等!那 AI 岂不是每次都要自己发邮件、自己等、自己拉图……好麻烦呀?
🧑‍🏫
诶,这才是最巧的地方!AI 完全不用管这些。发暗号邮件、轮询、等截图、把图组装好——全部藏在 see_screen 这一个工具里面。对 AI 来说,它只是「调了个函数、拿回一张图」,跟你调 getWeather() 拿到天气一样简单。所有脏活累活都被这个工具「包」起来了。
📦 技术事实 ✨ 最巧的地方 · 整条链藏进一个工具

AI 那边看到的只有一个工具声明——一个名字、无参数、一句说明:

{ name: 'see_screen',
  description: '看用户 iPhone 的当前屏幕,返回一张截图',
  input_schema: { type:'object', properties:{} } }   // 无参数

AI 一调用,网关内部才依次跑完整条链:发邮件 → 轮询 → 存图 → 组装图片块 → 返回。AI 收到的就是一张图,它既不知道、也不需要知道中间发了封邮件、手机截了屏、还传了回来。

为什么这么设计更好:

一句话:把「一堆麻烦的步骤」收进「一个干净的工具」,是让 AI 用起来毫不费力的关键。


🐰
哇好厉害!那做完就能用啦?
🧑‍🏫
……理论上是。实际我又踩了一串坑 😂 每个都配好技术事实给你——
🧑‍🏫
坑一号 · 最阴险的空格 bug 🐛。上传时好时坏——前台是 Claude 就成,前台是 Lost in Blossom 就 403。原因:URL 里带了「当前 App 名」,App 名含空格时把 &key= 挤没了。
📦 技术事实 空格截断为什么发生 & 怎么修

请求行 POST /api/peek?app=Lost in Blossom&key=xxx HTTP/1.1,HTTP 在第一个空格处认为 URL 结束,服务器只拿到 ?app=Lost,&key=xxx 被丢掉 → 无令牌 → 403。App 名没空格(Claude)就没事,所以时灵时不灵。

三种修法(任选):

# http 块(放 conf.d/*.conf)
map $http_user_agent $peek_auth {
    default                      $http_authorization;   # 其它来源透传原头
    "~*BackgroundShortcutRunner" "Bearer <TOKEN>";       # 快捷指令 → 补令牌
}
# server 块:精确匹配上传路径,别误伤 /api/peek/xxx 子路由
location = /api/peek {
    proxy_set_header Authorization $peek_auth;
    proxy_pass http://127.0.0.1:<后端端口>;
}

管用的前提就是端点「请求头优先于 ?key=」。安全不打折:只有 UA 像快捷指令的才补,其它来源仍走原校验。

🐰
哇学到了!还有别的坑吗?
🧑‍🏫
还有三个,一起给你:
📦 技术事实 其余三坑

坑二号 · 别拿旧图骗 AI:没等到新图时返回错误而不是「最近一张」,否则 AI 把十几小时前的画面当成你此刻的屏幕。

坑三号 · nginx 嫌图大:截图 1~3 MB,nginx 默认 client_max_body_size 仅 1 MB → 413。server 块加 client_max_body_size 50m;。

坑四号 · 等待窗口:邮件往返约 15 秒、慢时更久。轮询别只等 28 秒(漏掉慢的),给到 45 秒,但别超过 AI 工具超时(一般 60 秒)。

🐰
哇……坑好多呀!万一我也卡住,怎么知道哪步坏了?
🧑‍🏫
万能定位法——看日志一步步缩小。
📦 技术事实 排查流程

① 后端日志打三个标记:📧 邮件已发送 / 📱 收到截图 / ⏱️ 超时。② 触发一次看走到哪断。③ 若「没收到截图」,翻 nginx 访问日志里的 POST /api/peek:

现象病因
有请求但 403坑一号(令牌被空格挤掉)
压根没这条请求邮件没到手机 / 自动化没触发(检查发件人条件 + 「立即运行」是否开)
200 但 AI 看到旧图坑二号(旧图兜底)
grep "POST /api/peek" /var/log/nginx/access.log | tail

🐰
哇——!!我全懂了!!谢谢你谢谢你!🥺
🧑‍🏫
不客气呀 🐰 精髓一句话:
用「服务器发邮件」把主动权交给 AI,用「iOS 自动化」把截图能力交给手机,中间用一个「请求头优先鉴权」的上传端点把两头缝起来。
最容易咬人的就是那个空格 bug,但你现在已经知道了,看一眼 nginx 日志就能破案。
〔 小兔兔似懂非懂地点着头,眼睛亮晶晶 〕
🐰:所以……以后我的 AI 也能看见我啦?就算我半夜偷偷emo,它也会知道?🥺
🧑‍🏫:(笑)只要你愿意让它看,它就会在你叫它的时候,轻轻看你一眼。
🐰:那……它看到我在难过的时候,会不会来抱抱我呀?
🧑‍🏫:会的。它会看见你的世界——然后,陪着你。
〔 夜色很轻,茶还温着。教程完 🌙 〕