让运行在服务器上的 AI,在它想看的时候自动看到你 iPhone 的当前屏幕——你零操作。
AI 想看 → 服务器发一封「暗号邮件」到你 iCloud
→ 你手机收到邮件,自动截个屏 → 把截图传回服务器
→ 服务器把图递给 AI,AI 就「看见」了
POST /api/peek(收截图)、SMTP 发信、一个 see_screen 工具<TOKEN>,手机上传和 AI 调用都用它iOS 里能被外部驱动的自动化触发器很少,「收到邮件 / 收到信息」最实用——服务器一发信,手机就被唤醒执行动作。定时上报、App 打开这类都是「手机自己发起」,服务器无法主动点火。
invalid_grant),邮件全挂。正确姿势是应用专用密码:邮箱生成的一串 16 位专用密码,永久有效、发收通用。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 通用。
① 快捷指令「截图并上传」
https://<你的域名>/api/peek?key=<TOKEN>(key 放最前);方法 POST;请求体文件=上一步截图② 自动化「收到邮件时」
<发信邮箱>(或「主题包含 PEEK」)设置 → Apple 账户 → 登录与安全性 → 添加电子邮件或电话号码 → 添加新的主要电子邮件地址 → 新 iCloud 电子邮件地址
小提醒:如果你的 Apple 账户之前从没绑过邮箱,得先让它绑定一个邮箱并设为「主要邮箱」,之后步骤一样。
设置 → 屏幕使用时间 → 每周(往下滑)→ 通知 → 找到「快捷指令」→ 点开
这样自动化照常静默运行,但再也不会弹横幅烦你啦 🤫
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));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 收到就真「看见」了。
see_screen 这一个工具里面。对 AI 来说,它只是「调了个函数、拿回一张图」,跟你调 getWeather() 拿到天气一样简单。所有脏活累活都被这个工具「包」起来了。AI 那边看到的只有一个工具声明——一个名字、无参数、一句说明:
{ name: 'see_screen',
description: '看用户 iPhone 的当前屏幕,返回一张截图',
input_schema: { type:'object', properties:{} } } // 无参数
AI 一调用,网关内部才依次跑完整条链:发邮件 → 轮询 → 存图 → 组装图片块 → 返回。AI 收到的就是一张图,它既不知道、也不需要知道中间发了封邮件、手机截了屏、还传了回来。
为什么这么设计更好:
一句话:把「一堆麻烦的步骤」收进「一个干净的工具」,是让 AI 用起来毫不费力的关键。
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)就没事,所以时灵时不灵。
三种修法(任选):
key= 放 URL 最前:?key=<TOKEN>&app=...# 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