把抽屉里那块 GTR4 翻出来的时候,我本来只是想给它换个表盘。结果表盘商店里翻来翻去都是那几个风格,倒是我每天最需要的一个小功能——番茄钟——要么没有,要么做成手机上那种满屏按钮的简化版,根本没法在手腕上用。于是念头就歪了:我自己给它写一个。这个“自己写”,不是拿现成的 App 塞进去,而是在 Zepp OS 上从零做一个手表小程序,而且整个过程靠的是 AI coding。
整个过程下来,最大的收获不是产出了一个能用的番茄代办 App,而是验证了一个挺实用的工作流:一块两三年前的老表,一个懂点编程但完全没接触过手表开发的人,靠着 AI coding 一样能把想法变成戴在手腕上的应用。你不需要先啃完官方文档,也不需要是个前端高手,只需要知道怎么把需求拆清楚、怎么把 AI 的幻觉拉回正轨。这篇文章想把这条完整的路线记录下来:从开发环境、数据设计、计时逻辑,到 AI 翻车实录和真机安装,全部是这次实操里真实发生过的东西。
1. 旧表新用:为什么我想在 GTR4 上自己写个番茄钟
1.1 从抽屉里翻出来的 GTR4,到底还能干点什么
GTR4 是一款两三年前发布的智能手表,1.43 英寸圆形 AMOLED 屏幕,独立的 GPS,很长的续航,还有两个实体按键和一个线性震动马达。放在今天,它的智能功能确实不算新了,但作为一块表,基础素质依然优秀:屏幕够亮、震动够干脆、充一次电能顶一周多。它最大的问题不是硬件不行,而是应用商店里的东西太少,表盘翻来覆去就那么几套,官方应用也就那几个。
我重新激活它之后发现,它跑的系统是 Zepp OS,这套系统最大的特点是允许开发者安装第三方小程序,而且开发语言以 JavaScript 为主,不需要像开发别的表那样搞一套专门的环境。也就是说,只要你愿意写代码,这块旧表就可以具备任何你想象中的应用场景。当时我在手机应用商店里翻番茄钟,翻到的要么是订阅制的,要么是功能堆得密密麻麻的,要么根本不支持自定义。手表上更惨,系统自带的应用里压根没有番茄钟这个概念。
与其等官方更新,不如自己动手。这大概是所有“旧设备二次生命”故事里最有说服力的一点:设备本身没有坏,只是没有适合它的应用。而 Zepp OS 给了普通用户一个自己做应用的机会,这是它比同价位手表更值得折腾的地方。
1.2 为什么番茄钟必须长在手腕上
说来也怪,手机上番茄钟应用多得很,但我一直没有坚持用下来。原因也很简单:番茄钟的核心是“开始一段不被打扰的专注时间”,可手机本身就是最大的打扰源。开个番茄钟,锁屏放一边,屏幕一亮你总会忍不住瞄一眼通知,一瞄就可能滑走了。手机上的番茄钟在提醒你专注的同时,还在给你推送消息,这本身就是矛盾的。
手表不一样。它的屏幕小,信息密度低,抬腕只能看到“当前剩余时间”和“当前要做的事”,没有社交软件、没有信息流、没有短视频入口。番茄钟开始之后,你把手机丢在另一个房间,计时、进度、提醒全部由手腕上的震动和表盘完成。到了 25 分钟,手表震一下,告诉你“该休息了”,你抬手看看下一个待办是什么,继续干活就行,全程不需要碰手机。
所以这个需求的本质不是“我要一个计时器”,而是“我要一套能让我放下手机的专注仪式”。手表上的番茄钟,恰恰把手机从整个流程中踢了出去。这个感受很难用截图传达,戴上之后才明白为什么番茄钟一定要长在手腕上。
1.3 AI coding 到底帮了什么忙
说老实话,我写 JavaScript 的经验有一些,但完全没碰过 Zepp OS 开发。这套系统的 API、页面结构、生命周期、打包方式,对我来说全是新的。如果按照传统路线,我得先花两个晚上看文档,再照着官方模板改半天,才能跑通一个 hello world。但这次我全程用的是 AI coding,也就是通过对话让 AI 帮忙生成代码、修 bug、解释报错。
我负责的部分是:需求拆解、数据设计、边界条件、真机验证。AI 负责的部分是:页面布局代码生成、API 调用草稿、样式换算、把日志报错翻译成人话。这个分工不是一开始就定的,而是做了几个页面之后自然形成的。AI 在陌生平台上的第一版代码大概能跑通 60% 到 70%,剩下那部分不能用,绝大多数不是逻辑问题,而是它根本不熟悉 Zepp OS 的 API。后面我会详细说怎么喂上下文、怎么改错,这是 AI coding 能不能用好的关键。
2. 开发环境搭建:Zepp OS 应用与 AI 的第一次配合
2.1 开启开发者模式,准备一个最小的运行环境
开发手表 App 之前,先得让手表和设备进入开发者模式。不同固件版本的入口略有差异,但思路是一样的:手表端在“设置”里找到“关于”,连续点击版本号若干下,会弹出一个开发者模式开启的提示;手机端在 Zepp App 里找到开发者入口,开启后才能通过手机往手表安装本地应用。这一步卡了我一阵子,因为入口藏得比较深,后来我直接在 App 里搜“开发者”三个字才找到。
环境方面,电脑上需要安装 Zepp OS 的 CLI 工具。我用的是 npm 安装方式:
npm install -g @zeppos/zeppos-cli安装完成之后创建一个项目模板:
zeppos create它会生成一个带 hello world 的脚手架工程。我第一次跑成功之后,做的第一件事不是自己去看代码,而是把这个模板工程的关键文件丢给 AI,让它先读一遍,再跟它说“我们要在这个基础上做一个番茄钟应用”。这个习惯在后面帮了大忙:AI 只有见过真实的工程结构,写出来的代码才不至于跑偏。
模拟器我也装了。Zepp OS 的模拟器可以单独下载,不过我后来发现,手表真机就在手边,插上充电线就能调试,大部分时间我直接跑真机。模拟器适合先验证视觉布局,真机适合验证震动、息屏、续航这些模拟器给不了的真实感受。
2.2 让 AI 从零生成一个手表 App 的正确姿势
很多人用 AI 写代码,上来就是一句“帮我写个番茄钟 App”,然后拿到代码不知所措。在手表小程序这个场景里,这么问基本必翻车,因为 AI 不知道 Zepp OS 是什么,只会按它最熟悉的微信小程序或者网页应用来写。它给你写出wx.setStorage的时候,你根本跑都跑不起来。
我摸索出一套提示词结构,效果稳定很多:
第一句交代平台和限制:目标平台是 Zepp OS 2.x 手表小程序,用 JavaScript,不能用浏览器 API,不能使用可能存在的外部依赖,只允许使用提供的模板工程中出现的模块。
第二段是背景:把一个最小可运行工程的代码贴进去,告诉 AI“这是官方模板,所有接口调用请参考这里的写法”。
第三段是交互描述:首页显示一个大号倒计时数字,上方显示当前待办标题,下方有开始、暂停、重置三个按钮;待办页可以添加新待办、勾选完成、删除待办;每次点击开始要记录一条番茄钟完成记录。
第四段是约束:页面切换不要动画,点击区域要大于多少像素,字体要尽量大,适配低分辨率屏幕。
关键点在于让 AI “先读模板再写代码”,而不是让它凭印象发挥。我在几次翻车之后发现,只要我事先把模板代码贴给它,它生成的第一版代码能跑通的比例就会高很多。它会把模板里那些正确的 API 导入语句、生命周期写法复用到新代码里,张冠李戴的概率大幅下降。
2.3 降低 AI 幻觉的几个操作细节
AI 写代码最大的问题不是不会写,而是“一本正经地编造 API”。它可能写出一个看起来逻辑完备、但方法名在 Zepp OS 里根本不存在的函数。我踩过几次坑之后总结出几个很实用的操作细节。
细节一:先让 AI 列接口清单再写代码。在我描述完需求之后,我会加一句“先列出你打算使用的所有 API 名称和来源模块,等确认之后再写代码”。这一步能帮我把 AI 的幻觉扼杀在摇篮里,如果它列出来的接口在官方文档里根本不存在,我会直接把文档对应段落贴给它,告诉它“项目中不存在这个方法,你只能用文档中出现的接口”。
细节二:每次只生成一个页面或一个模块。让 AI 一口气生成整个应用,结果往往是每个页面都有小缺陷,改起来互相牵连。我改成“先写首页计时器,跑通之后再写待办页,最后写存储模块”,每一步都有清晰的验收标准,出了 bug 也知道是哪个文件的问题。
细节三:把模拟器或真机的报错原样贴回去,不要自己概括。我一开始习惯把报错理解成一句话再告诉 AI,比如“它说存储出问题了”,效果很差。后来我直接复制完整日志,附带相关的代码块,让 AI 自己分析。AI 读原始日志的能力比读二手转述强得多,这和跟同事协作是反过来的。
3. 番茄代办 App 的核心实现:从数据设计到计时逻辑
3.1 数据模型先想清楚:待办、计时与记录的存储设计
写手表应用之前,我先把要存的东西全部列了一遍。这个 App 需要的数据大概有四类:待办列表、当前选中的待办、番茄钟完成记录、用户设置。Zepp OS 提供的是一个键值对存储模块,读写方式接近Storage.get(key)和Storage.set(key, value),没有本地数据库,所以所有结构化数据都得序列化成 JSON 之后存进去。
我的键设计如下:
todos_v1:待办数组,每一项包含{ id, title, done, createdAt },对应一个待办事项。currentTodoId:当前选中的待办 ID,计时结束时方便记录“刚才专注的是哪件事”。focusRecords:番茄钟记录数组,每一项包含{ todoId, todoTitle, startTs, durationMin }。settings:包含工作分钟数、休息分钟数、每日目标等配置。
这个设计看起来简单,但有一个坑:手表存储空间很有限,如果每条番茄记录都无脑追加,早晚有一天写满。所以我在写入focusRecords时做了两个限制:只保留最近 30 条记录,超过就把最老的覆盖掉;每条记录的durationMin只在重新开始时更新,不做秒级写入。这样既保证统计功能有数据可用,又不会让存储模块成为性能瓶颈。
选 kv 而不是数据库还有一个原因:读写频率低。番茄钟的典型使用场景是几十分钟才写一次存储,压根用不到事务和索引。用最简单的存储模型,反而更不容易出错。
3.2 页面结构:大数字、大按钮和一只手能操作的单手流
手表不是手机,屏幕小、触控精度差,交互逻辑必须做减法。我最终保留了三个页面:首页、待办页、新增页。首页是核心,占据 90% 的使用时间。
首页上半部分是一个显示剩余分钟和秒的大数字,字号恨不得撑满半块表盘;大数字下方一行小字显示当前正在专注的待办标题;再往下是两个大按钮,左侧“开始/暂停”,右侧“重置”。整个页面只有一个动作路径:选中一个待办,点开始,抬腕看进度,震动结束。新增待办不需要常驻首页,全部收进待办页,避免误触。
AI 一开始生成的 UI 是手机上常见的“标题栏 + 列表 + 底部按钮”结构,字体小、按钮窄,我戴到手腕上试了一下,根本点不准。改版时我只提了一个要求:所有可点击区域最小 40 像素,时间数字用一个专用的巨大字体,其他文字全部退居次要位置。手表应用的设计原则和手机完全是两码事,手机上讲究信息层级丰富,手表上只讲究“抬手三秒内看清关键信息”,做不到这一点,功能再全也没用。
3.3 计时状态机与息屏恢复:这步是全场最关键的坑
手表应用和浏览器应用最大的区别在于:屏幕一熄,JS 定时器基本会被系统挂起。如果像网页里那样写一个setInterval每秒减 1,那么息屏一分钟再抬腕,倒计时就落后了整整一分钟。这大概是手表端番茄钟最容易翻车的点,也是 AI 完全没概念的地方。
我的方案分三层。第一层,不依赖定时器累计,而是保存“目标结束时间戳”。开始按钮按下时,计算endAt = Date.now() + workMinutes * 60000,把endAt写进存储。第二层,页面在每次显示时,也就是生命周期回调触发的时刻,用Date.now()重算剩余秒数。第三层,屏幕可见期间才做秒级刷新,刷新只负责更新 UI,不再参与计时判断。核心逻辑简化下来是这样的:
function computeRemainingSec() { const endAt = getSetting('endAt'); return Math.max(0, Math.round((endAt - Date.now()) / 1000)); }倒计时状态机也不复杂,总共四个状态:空闲、专注中、休息中、已完成。所有状态迁移都收进一个统一的函数处理:
function go(state) { currentState = state; setSetting('state', state); if (state === 'focus') { setSetting('endAt', Date.now() + workMin * 60000); } render(); }这里最关键的一行是setSetting('state', state)。手表应用随时可能被系统杀掉,如果把状态只放在内存里,用户抬腕一看,它可能已经悄悄退回了空闲状态。每次状态迁移都持久化,重启之后才能恢复现场。AI 最初写的版本没有这一行,导致我直接锁屏再抬腕,番茄钟就变成“已结束”。这个坑修了我整整一个晚上,也是整篇文章里含金量最高的一课。
4. AI Coding 翻车实录与排坑方法
4.1 翻车场景一:API 张冠李戴
第一次跑番茄钟首页的时候,AI 生成了一段存储代码,用了Storage.setValue('todos', todos),我放到模拟器一跑,直接报错“setValue is not a function”。查了下文档,Zepp OS 里这个模块的接口叫set,而不是setValue。还有一个更典型的错误:它曾经给我写过wx.setStorageSync,这是微信小程序的 API,在手表上自然不存在。
这类错误最好解决,因为思路很简单:把官方文档相关段落直接贴进对话,附上一句“这个运行环境不是微信小程序,不是 React Native,是 Zepp OS,只允许使用文档里出现的接口”。AI 看到真实文档之后,极少会再犯同样的错误。真正的坑是它下次写新功能时可能又犯类似问题,所以我每次新开一个文件时都会重复一遍这个“平台约束”声明,宁可啰嗦也不放松。
4.2 翻车场景二:跨页面数据不同步
AI 写待办页的时候,习惯性地在页面内部定义一个数组变量来存待办列表,点击“新增”后推一条进去。看起来没问题,但切到首页再切回来,新增的待办消失了,因为页面重新加载时变量被重置,根本没写进存储。
这个问题的根源是 AI 把网页应用里“全局变量即共享”的思维带过来了。手表页面之间的数据同步,必须通过存储模块来做。我的解决办法是抽了一个统一的 store 模块,所有页面对待办的读写都走它,页面内部不再自己维护数据:
import { Storage } from '@zos/storage'; const TODO_KEY = 'todos_v1'; let cache = null; export function getTodos() { if (!cache) { cache = JSON.parse(Storage.get(TODO_KEY, '[]')); } return cache; } export function setTodos(next) { cache = next; Storage.set(TODO_KEY, JSON.stringify(next)); }改完之后,待办的新增、勾选、删除全部通过这个模块完成。这个决策属于典型的系统设计问题,AI 不会主动想到,需要人来拍板。这也是我反复强调“AI 擅长写代码,不擅长做架构”的原因。
4.3 翻车场景三:AI 连续改错时的正确拉法
有一次我很想让首页的数字更大一点,改成“每 5 秒刷新一次大字”的需求。结果 AI 连着改了三次,每次看起来都在认真修,但一跑就崩。第一次改了字体变量,把渲染函数里一个参数拼错了;第二次改了布局,把整个数字区域的宽度算错;第三次最离谱,直接把倒计时归零了。
后来我意识到问题在于:我把需求描述得太宽泛,“让数字更大”这个指令 AI 可以发挥的空间太大。它改的时候会波及到无关代码。于是我换了个策略,直接贴出formatTime这个函数的完整代码,然后说“只允许修改这个函数内部,不要动其他文件”。这次它一次就改对了。
这个经验非常实用:AI 连续改错时,先不要继续加需求,而是把问题收敛到一个函数、一个文件、一个变量的范围内,把上下文收到最窄,再让它动手。给 AI 越大的自由度,它越容易在无意中破坏现有逻辑;给它明确的边界,它反而能稳定发挥。
5. 上手腕实测:打包、安装与日常使用感受
5.1 从模拟器到真机:安装一个自己写的 App
模拟器上跑通之后,下一步就是打包安装到真机。Zepp OS 的 CLI 工具提供了构建命令,生成一个后缀名为 zpk 的安装包。安装路径一般是通过手机上的 Zepp App 的开发者模式,选择“安装本地应用”,然后选中这个 zpk 文件。我第一次装的时候找这个入口找了十分钟,最后是在 App 设置页里搜“开发者”三个字才搜出来的。不同版本入口名字会不一样,找不到就别硬找,直接搜关键词。
装好之后,手表应用列表里会出现一个默认图标的程序,名字是我在项目配置里写的。首次打开它时,系统会提示申请振动权限,这个一定要同意,因为番茄钟的完成提醒全靠震动,没有震动就没有存在感。应用安装完成并不代表万事大吉,我第一次真机运行时才发现,模拟器里显示正常的动画,真机上有明显的掉帧,于是又把页面切换动画全部关掉了。模拟器永远代替不了真机,这句话在手表开发里尤其正确。
5.2 五十次番茄钟之后:续航、跟手度与摘不下来的理由
装好之后我连续用了两周,跑了五十多个番茄钟。先说续航。GTR4 本身的底子就好,平时用一周多一充,我的番茄钟 App 不打 GPS、不联网、不做疯狂刷新,只在抬腕亮屏时渲染一下数字、到时间震一下,对续航的影响微乎其微。两周用下来,充电频率和之前基本一致,没有明显感知。
再说跟手度。手表触控屏的灵敏度比手机差一截,按钮做小了真的会点不到旁边去。我把所有按钮的点击区域加大之后,操作成功率明显提升,误触情况几乎消失。最舒服的一点是抬腕看进度:屏幕一亮的瞬间,剩余分钟数已经按新的时间戳算好了,不需要等待刷新,这个体验是手机番茄钟给不了的。
戴了两周之后,我觉得这块表摘不下来的理由也很简单:手机番茄钟需要你主动打开 App、保持屏幕常亮、忍受通知干扰,而手表番茄钟只留下了一个动作——开始,剩下的都由震动和表盘接住了。专注这件事,并没有因为它戴在手上而变得神奇,但它确实被简化到了没法再简单的程度。
5.3 可以继续往深里做的几个方向
这个番茄代办 App 目前的功能已经满足日常使用了,但我心里已经列了一串想加的东西。
统计页是第一个想做的。现在focusRecords里累计了几十条记录,但手表上没有页面展示,数据白白躺着。我可以按天、按周聚合专注时长,用表格或简单柱状图呈现,这样每周日晚抬腕看一眼,就能知道这周的工作节奏。
每日目标也值得加。如果设定“今天完成 6 个番茄”,那么每完成一个,表盘上就能看到进度环在收拢,直到目标达成后震动庆祝一下。这个机制对习惯养成很有帮助,它会让你在下午四点多的时候觉得“还差两个番茄,不如再开一轮”。
还有一个方向是跟通知联动。Zepp OS 支持消息推送,手机来通知时手机会震。现在我的处理方式是专注模式开启后,手机直接进勿扰,但有些消息又不想错过。后续可以做一个配置项:专注期间只放行特定应用的通知,其余全部静默,休息时再统一提醒。这个需求很有真实价值,做起来也不复杂,只是需要在手表端和手机端两头都要改逻辑。
最后,就算这块表真的要退役,这套代码本身也是有价值的。把数据模型和计时逻辑打包成一个模板,将来换别的 Zepp OS 设备时,只需要调整页面尺寸和图标设计,重新构建一下就能继续用。旧设备会淘汰,但写过东西的经验和处理问题的思路不会。
我在实际使用中最深的一点体会是:AI coding 并不是“我要什么它给我什么”的魔法,它更擅长的是把已经很明确的指令翻译成代码的执行层工作。系统边界、状态迁移、数据一致性这些真正决定应用能不能长期稳定运行的架构决策,依然得靠人来想清楚。但反过来讲,一个能把需求拆得足够清晰、愿意逐字核对文档边界的人,配上 AI coding 之后,产出效率比过去手写提升了一倍还不止。几天前还躺在抽屉里吃灰的一块旧表,现在每天陪我完成七八个番茄钟,这种满足感大概就是技术折腾最原始的动力。