给 AI 管家做个手机版:从「想在手机上用」到装进 iPhone 主屏

发布时间:2026/10/1 20:01:24
给 AI 管家做个手机版:从「想在手机上用」到装进 iPhone 主屏 给 AI 管家做个手机版从「想在手机上用」到装进 iPhone 主屏我的 AI 管家跑在家里那台机器上。它能替我干活——查数据、跑脚本、写东西、盯任务——但有个前提我得坐在电脑前。一旦离开电脑它就从「助手」变成了「黑盒」我知道它在干活却看不见它干到哪一步、也接不上话。我想解决的不是功能问题是位置问题。下面这一天里发生的事方案定稿 → 功能一次做全 → 装进 iPhone 主屏 → 上线中间 22 轮迭代。踩的坑我全写在这儿了包括几个「只有真机才会暴露」的。本文由 AI 辅助创作人工审阅后发布。一、起因问题不在功能在位置具体一点说我当时的困扰是四条直接在手机浏览器里访问它适配很差——那不是给手机做的页面看着难受点着更难受。外网根本访问不了——它只在家里那台机器上出门就断联。它自己没有登录、没有权限体系——直接把端口怼到公网上等于把整个 API 裸奔给全世界这点后面还会说它是整个设计的前提。用聊天软件虽然能发消息但看不到历史、也切不了会话——想翻回上一条结论做不到。所以需求很清楚一个手机浏览器打开的轻量前端能切会话、能发消息、能看历史并且要能安全地在外面用。听起来像个小项目。但它踩的坑比功能本身多得多。二、动手之前先把底摸清我一开始克制住了「先写代码」的冲动花时间把环境摸了一遍。事后看这一步省下了最多的返工。① 它的 API 是干净的 REST SSE。列会话、看历史、发消息、停止生成、上传文件、审批工具授权都有对应的接口发消息是流式的SSE服务器推着字往外出。这意味着前端可以写得很薄——不用为了它专门改后端。② 也是最关键的一条它没有鉴权。它的鉴权状态接口明确返回「未启用、无用户」。也就是说谁连上这个端口谁就能看到我的全部会话、也能替我发指令。这一条直接决定了整件事的形态前端页面可以随便做但必须有一道门挡在前面而且这道门不能做在页面里页面里的密码框等于把「进门的钥匙藏在门垫下面」——接口本身还是裸的。③ 手上有现成的隧道和现成的门禁。之前为了别的事情已经搭好了两样东西一条从家里通到云上服务器的反向隧道和一套「nginx 层口令门禁」——没输口令连 HTML 都不下发种下 cookie 之后 30 天内免输。于是方案就很朴素了只写前端剩下的全部复用。不新买机器、不开新域名、不动后端。三、方案把口令挡在 nginx 那一层最终的链路是这样手机浏览器 │ HTTPS ▼ 云服务器 nginx ── ① 口令门禁挡住整个路径下的一切 ── ② 前端静态页就是几个静态文件 ── ③ /api/ 反向代理关掉缓冲、拉长超时 ▼ 反向隧道 ⇄ 家里那台机器 ⇄ AI 管家自己的 API三条设计决定都是在「省事」和「安全」之间取了交集复用已有的路径不开新域名、不申请新证书门禁做在 nginx 层server 级拦截 cookie页面本身不需要登录逻辑前端的每一个请求天然被护在里面前端所有请求走同源页面和 API 在同一个域名下不引入跨域也不需要在 JS 里管理任何凭证。截图里的口令框是空的页面标题打了码。门禁第一版上线前我把「不输口令会怎样」当验收项跑了匿名访问只能拿到一个口令页拿不到任何真实内容——门禁不是装饰它就是那道锁。四、这一天先把功能一次做全功能我是一次做全的切会话、发消息、看历史、传图传文件、语音输入、工具授权审批、模型与智能体切换。事后看这个决定是对的——这些功能互相咬合比如审批卡片是长在消息流里的分期反而更麻烦。会话名已打码。会话列表有两个坑坑一列表里混着不该出现的东西。API 返回的会话列表里混着定时任务的会话和子代理的会话——这些不是「我在跟它聊天」而是系统自己在跑。前端必须按字段把它们滤掉否则列表会被每天的定时任务刷满。这个过滤一开始还漏了一种形式有的定时会话 id 里带前缀类似平台:用户:cron:xxx而我的判断条件是「以 cron 开头」——漏了。收紧之后列表从 75 条变成 55 条干净了。坑二这个我印象最深排序整个失效而且一声不吭。我一开始的排序代码长这样list.sort((a,b)(b.updated_at||0)-(a.updated_at||0));看起来没问题但服务端给的updated_at是ISO 格式的字符串类似2026-09-29T04:07:12.948Z。字符串相减得到的是NaN。而Array.sort的规定是比较函数返回NaN时当作「两者相等」处理。结果就是排序被静默跳过列表老老实实按接口返回的顺序最旧的在前显示——不报错、不警告、什么都不提示。我盯着它看了半天一度以为是自己排序规则写反了。修法不难把时间先归一化成数字再比但教训挺值钱弱类型语言里的「静默失败」比报错难查一百倍。顺带还有个体验细节既然要求「最近的放最前」那么被置顶的会话就不该强行顶到最上面——现在置顶只保留一个灰色小标签信息不丢顺序不乱。新会话的自动命名要跟服务端配合新会话原本永远叫「新会话」。查了服务端的实现才知道它的自动起名有个前提建会话时名字要先等于首条消息的前 10 个字——服务端据此生成标题。照这个逻辑接上之后新会话第一次发言完名字就自动变成了像样的标题。这种「读一眼对方源码再动手」的事猜是猜不出来的。五、真机才是考场功能在电脑上全绿之后我发到手机上试。第一次反馈就三件事输入框不对齐、顶栏被压住、键盘一弹输入区就废。我在无头浏览器里怎么都复现不出来只能看着真机截图一个个量。① 输入框不是「差点没对齐」是被压成了 19px真机截图是 1170×2532也就是 390×844 的 CSS 尺寸我把上下边框的位置量出来输入框只有约19 个 CSS 像素高——正常应该是 42px。它的上下内边距本身就要占 18px等于内容区被压到了零文字被挤出框外。根因不在 CSS在调用时机内容是先写进输入框、再显示聊天视图的。而写进输入框时会去自动调整高度读一下内容高度、把高度设成那个值——那会儿容器还是隐藏状态没有布局读出来的高度恒为 0于是高度被设成了 0靠浏览器兜底撑成 19px。三处一起修调整顺序先显示再回填、加一道「不可见就不测量」的保险、再补上box-sizing吃掉的那 2px 边框CSS 里顺手兜一层最小高度。② 安全区只加了一半顶栏的标题和右上角的控件被 iPhone 的状态栏压住了。原因很朴素安全区的内边距只加在了列表页聊天页漏了——于是聊天页的顶栏老老实实顶到了刘海底下。③ 键盘弹起来输入区被盖住iOS 不会因为你把输入区固定position: fixed就把它顶到键盘上面去它只会盖住你。得自己算拿「窗口高度 − 可视视口高度」得到真实遮挡高度再把输入区顶上去。④ 底部那 47px不是内边距是浏览器不给这一条最值得说。输入区下方始终留着一条空白。我先按内边距查量完发现不对输入区规规矩矩贴着布局视口的底边只是那个「布局视口」只有 797px而屏幕是 844px——差的 47px 不在页面里页面画不到那儿。这是 iOS Safari 的老毛病地址栏收起之后它不重算页面的初始包含块于是页面一直卡在小尺寸上。我的处理是承认这件事不跟浏览器争这块地改底色。把页面画布的底色改成和输入区一样的颜色那条缝就从「灰色空白」变成「输入区底色的延伸」看不出接缝。深色浅色各一套。说句实话这是「视觉补齐」不是真铺满。输入框的底边仍然在离屏幕底 47px 的地方——那块地不归网页。哪天浏览器肯给满屏这段逻辑自动就多余了不用改代码。顺便我在这儿埋了个诊断点页面每次打开都会把「它实际拿到的视口高度」回报到服务器。这样「到底是不是浏览器不给」不用猜——看日志里那个数字是 797 还是 844 就知道了。六、把它变成「App」图标、清单、启动图页面能用之后下一个诉求是能不能像 App 一样放在主屏上图标丑的真因不是审美问题iOS 支持「添加到主屏幕」但这个页面当时既没有主屏图标、也没有应用清单——于是 iOS 只能拿网页截图当图标。所以主屏上那个东西特别丑它不是设计得丑是它压根不是一个图标。补齐的东西不多一张 180×180 的主屏图标、一份应用清单声明成独立应用、给个名字、16 张启动图8 种机型 × 深浅两套——这样从主屏冷启不会再白屏闪一下。图标的三个版本和「不作拉伸」的做法图标我做了三版给用户挑最后定的是「满底橙、点阵花」那一版不要任何框图标本体就是那块橙。这里有个技术细节我挺喜欢用户给的源图只有126×112上面的点直径才3~8 像素直接放大会糊成马赛克。所以我没有拉伸原图而是把那49 个点逐个量出来中心 1 点 8 层环 × 每层 6 点每一层比内层整体旋转约 33°——这个旋转正是源图那种「漩涡感」的来源再用矢量圆重新画成 1024 的母版。与源图逐点比对误差0.39 像素等于逐点复刻但边缘是干净的。iOS 的规矩图标只在「添加」那一刻抄这一步有个必须知道的规矩**iOS 是在你点「添加到主屏幕」的那一刻把图标、名字、启动图抄下来的。**之后服务器上怎么改主屏那个图标都不会自动更新。所以每次换图标用户都得手动走一遍长按旧图标 → 移除 → 重新打开页面 → 添加到主屏幕。另外 iOS 是按 URL 缓存主屏图标和启动图的URL 不变可能继续用旧图——为此给这些资源加了个版本号参数?vbloom来强制刷新。七、用起来之后才出现的三个真问题到这一步「做出来」的部分结束了。真正有意思的部分从这里才开始只有每天都用它才会遇到的问题。① 锁屏回来屏幕上只剩一句Load failed用完手机锁屏再打开页面就一句红色的Load failed什么都不剩。看起来像「任务挂了」但我去核对了服务端代码客户端断开只是取消订阅那一轮任务本身照跑结果照样落库。换句话说用户截图里那一轮其实早就跑完了内容一条没丢——只是页面没去把它取回来也不知道该去取。顺便说这条「断开 ≠ 失败」的认知是整件事里最重要的一条。它决定了修复的方向——不是「重试请求」而是「接回来」现在的设计是两步断线时不再只丢红字改成一张虚线卡片「连接中断了。手机锁屏 / 切后台容易断任务本身还在跑接回就能补齐。」 一个【继续接回】按钮但多数情况根本不用点回到前台、从主屏重开、网络恢复都会自动接回先问服务端状态还在跑就接上增量跑完了就直接取结果自动重试三次不成功才轮到手动按钮。关键差别在「补齐」接回不只是接续后面的输出而是用服务端落库的历史覆盖页面——所以锁屏几分钟回来看到的是一份完整回答而不是半截。还有个安全阀如果拉回来的内容比屏幕上已有的还短说明服务端还没落完就等一会儿重试服务端一条都没有时绝不覆盖本地——免得把已经读到的字擦掉。② 语音发完输入框里还留着字用户反馈说话发送之后输入框里又冒出刚才那段文字。根因是识别引擎没被停掉我把「发送」和「停止识别」的顺序搞反了——发送之后识别引擎还会迟到一次结果那个回调把已经发出去的文字又写回了输入框更糟的是还存成了草稿重开这个会话又回填一次。修法就是发送时先静音识别、再停引擎迟到的结果一律丢弃。这条我特意没有「凭感觉改」先写了个复现脚本拿未修复的版本跑——真的红了输入框里冒出「你好世界。」跟用户描述一模一样再换修复版全绿。③ 重进正在跑的会话手机端毫无反应用户又报切回列表、再点进那个正在跑的任务页面什么都不动。这次是三个 bug 叠在一起上一轮对话没收干净导致「正在流式输出」这个状态永久卡在 true于是「接回」的调用被入口守卫挡了回来而重渲染消息时又用整块重写的方式把流式输出写进了一个已经被摘出页面的旧卡片里。修完之后加了兜底12 秒没动静就自动检查一次「是不是僵尸流」自己修复。验收数字也很直观——修复前 90 秒一个字没出修复后1~2 秒就看到 315 个字在滚。八、通知让「跑完了」这件事找到我我最真实的使用场景是在手机上发一条长任务然后把手机锁上去干别的事。所以「跑完了得让我知道」是个刚需。这个功能我改了两次第一版走的是第三方推送 App能响、能穿透专注模式。但很快发现一个硬伤它点开是跳进那个 App而不是跳回我的页面——我希望点开就直接进「AI 管家」这个网页应用落在对应的会话里。第二版换成了 Web PushiOS 16.4 之后从主屏打开的网页应用可以自己发通知。代价是多了三个前提系统版本要够、必须先从主屏图标打开再开通知开关在 Safari 标签页里开不了、别从后台把它彻底划掉划掉会连通知一起停。现在的行为是长任务跑完 → 手机收到一条通知会话名 回答前 70 字 耗时→点开直接进 App 并落在那个会话。太短的任务不到 60 秒不打扰。「通知里能不能直接点『批准 / 拒绝』」问这个问题的场景很具体任务卡在「需要授权某个操作」上等我点确认我在外面希望直接在通知上点一下。结论是不能而且这是平台限制不是实现问题。网页通知的标准里有个「按钮」能力Notification.actions但它在 Safari 和 iOS 上都没有支持——只有原生 App 用自己的通知接口才能做到。所以最好的效果只能是点通知 → 进 App → 落在该会话的审批卡片上 → 点一次「允许」一共两次点击。这已经是这条路线的天花板了。九、最值得说的一条工程判断审批通知等 120 秒最后这段是整件事里我最想分享的一段——因为它不是技术问题是判断问题。需求原话是「能不审批就不审批只有一定要审批、而且真卡住了才通知到我手机上。」我第一版做得很直白一看见有「待审批」就推通知。然后就被自己的手机教育了。某天 23:10我收到一条「⏳ 任务卡在等你确认」——点进去一看它 20 秒后自己就被批准了。我根本没察觉有这回事通知纯属虚惊。于是我去翻数据看「审批到底通常要等多久」当天可配对的记录 259 笔指标数值批准耗时中位数8 秒75% 的审批在32 秒内完成90% 的审批在102 秒内完成超过 120 秒才被批的只有 23 笔服务器自动拒绝的等待上限300 秒数据把话说清楚了绝大多数审批是在几秒内完成的——因为通常情况下人就在电脑前顺手点了。所以我给通知加了一个等待门槛默认 120 秒。审批满了 120 秒还没人理才推给你正文里还会写明「已经等了约 N 分钟」「再没人应它就会被自动拒绝」。这条修完我还顺手纠正了自己一个错误认知我一度以为「审批通道是哑的、根本没在用」。查完发现当天真实发生了 363 笔审批——因为它的工具守卫有一份内置的默认清单包含执行命令、读写文件这类危险操作即便审批级别设成「自动」危险操作该拦还是会拦。**这次修正背后的那句话比代码值钱通知不该报「有事件发生」该报「真的卡住了」。**前者只是把系统的忙碌转嫁给人后者才是「需要人」。十、代价和边界诚实清单写到这里都是成果但这些是必须一起说清的代价后端 API 本身没有鉴权所以口令门禁就是那把锁。它不是账号体系——谁拿到口令谁就能看到全部会话、也能替我发指令。这一点我一开始就知道也接受它是单用户设计但它意味着口令本身就是最高机密。**这套东西跑在一个「可写层」上不是持久卷。**重启没事数据都在但如果哪天把容器删了重建工作区里的记忆、项目、配置会一起没。要升级就先备份。**常驻进程的配置冷启动时可能被重写掉。**我加的后台推送进程和通知进程一度只存在于「运行时」的配置里——容器启动脚本会用模板无条件重写那个配置文件一断电重启就静默消失。发现后补进了模板现在断电恢复才真的可靠预期 3~6 分钟。**真机验证不可替代。**安全区、键盘、视口这几个 bug在无头浏览器里一个都复现不出来那些环境变量恒为零、布局视口恒等于可视视口。所以每个真机 bug 我都补了一条「机制断言」进验收脚本防止以后再犯——但发现它们仍然只能靠真机。十一、小结回头看这个项目的功能部分其实不复杂一个薄前端几个接口。真正花时间的全是**「用起来之后」才暴露的东西**锁屏断线、语音回填、视口不给满屏、通知要等够 120 秒才值得发。如果只允许留一句话我会留这条「能用」和「好用」之间隔着的不是功能清单是一堆只有真用起来才会撞上的坑。而这些坑没有一个是靠「想清楚」能提前绕开的——它们只会在你把它放进兜里、锁上屏、走出门之后才一个个冒出来。(本文由 AI 辅助创作人工审阅后发布。文中截图均为真实使用界面会话名称与账号信息已打码脱敏。)

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询