简介:这份人工智能实战微信小程序Demo面向希望把AI能力落地到微信端的开发者、学生与课程设计者,提供可直接导入微信开发者工具运行的完整示例工程,可用于学习小程序页面组织、组件拆分、录音与播放、待办清单、消息展示及二维码等常见交互的实现思路。压缩包共59个文件,约1.36MB,以23个png及gif、jpg界面素材,12个JavaScript逻辑脚本,7个wxss样式,5个wxml模板和5个json配置为主,并附README说明,体量轻便但结构较完整,便于按页面与组件分层阅读。目前已有243人学习下载,适合作为人工智能小程序入门实战、课堂演示或二次开发的基础模板。读者可从中获取页面骨架、样式组织、工具函数、录音播放与数据记录等模块的参考写法,也能借助现成素材快速复现界面,减少从零搭建环境与目录结构的成本,进一步理解AI功能在小程序中的接入位置与项目组织方式。
1. 拆开这个包之前,先想清楚它替我省了什么
人工智能实战微信小程序demo.zip 解压出来不是一堆讲课 PPT,而是 pages、cloudfunctions、utils 三个实实在在的目录。它把「拍照或选图 → 压图 → 传给服务端 → 拿到结构化结果 → 渲染到页面」这条链路完整跑通了一遍,中间那些最容易翻车的环节——密钥放在哪、图片压到多少、接口超时怎么兜底——都留下了可改的痕迹。适合已经会写小程序页面、但没真正把 AI 能力接进产品的开发者,也适合临时要搭一个能当面演示的原型的团队。接下来按拆包、跑通、调参、避坑的顺序往下走。
2. 拆包先看骨架:前端页面、云函数与外部服务的三层边界
拿到压缩包我一般不会立刻拖进开发者工具,而是先在终端里把目录结构过一遍。原因很实在:这类 AI 小程序 demo 最常见的结构是三段式,前端页面负责交互和渲染,云函数负责鉴权、拼参数、转发请求,外部 AI 服务负责真正干活。三层之间只要有一层边界被写糊了,比如密钥直接写进前端 js,后面无论怎么调都是在给自己埋雷。先看清边界,再决定哪一层可以动、哪一层最好别碰。
2.1 目录骨架与三层职责的对应关系
一个典型的人工智能实战小程序包,目录大致如下。不同作者的命名习惯会有出入,但职责划分基本一致。
| 目录或文件 | 承担的职责 | 改动风险 |
|---|---|---|
pages/ | 页面结构、事件绑定、结果渲染 | 低,随便改 |
utils/或services/ | 请求封装、图片处理、结果格式化 | 中,改错会影响所有页面 |
cloudfunctions/ | 密钥持有、参数拼装、上游调用、错误收敛 | 高,改错整条链路断 |
app.json | 路由注册、权限声明、tabBar 配置 | 中,漏配直接白屏 |
project.config.json | 编译根目录、云函数根目录指向 | 低,但填错就找不到云函数 |
这张表最该记住的是第三行。云函数目录是整份资源里唯一持有敏感信息的地方,也是唯一一个「前端看不到、出问题只能靠日志排查」的黑匣子。新手容易在这里犯的错是把云函数当成普通 js 文件,在里面随意加日志打印,把上游返回的完整报文连同密钥一起打到客户端,结果就是演示当天一切正常,上线第二天被人扒出接口地址。
我一般的做法是:先把cloudfunctions/下每个子目录都看一遍,确认每个目录里都有index.js和package.json。缺package.json的函数大概率是作者本地部署完忘记提交,你导入之后右键上传会直接报错,这类残缺在压缩包里很常见,属于血泪经验。
2.2 三个必须先打开的配置文件
在写任何业务代码之前,有三个文件必须先看清楚,它们决定了这个包能不能在你自己的账号下跑起来。
第一个是project.config.json,重点是两个字段:
{ "miniprogramRoot": "miniprogram/", "cloudfunctionRoot": "cloudfunctions/", "setting": { "urlCheck": false, "es6": true, "enhance": true } }miniprogramRoot指向小程序代码的实际根目录,有些包会把它设成./而代码其实在miniprogram/下,导入后页面列表是空的,就是这里对不上。cloudfunctionRoot决定开发者工具能不能识别出云函数目录——只有当这个字段指向的目录存在时,工具左侧才会出现云开发面板和「上传并部署」的右键菜单。urlCheck: false表示工具里关掉了合法域名校验,这个设置本身是开发期的便利,但也正是因为它在,很多域名问题要等到真机才暴露,后面避坑章节会专门讲。
第二个是app.json,重点看pages数组和permission段:
{ "pages": ["pages/index/index", "pages/vision/vision"], "permission": { "scope.userLocation": { "desc": "用于展示附近的结果" } }, "requiredPrivateInfos": ["chooseMedia"] }pages里没注册的页面,即使文件存在也访问不到,这是导入别人包之后「点了没反应」的第一嫌疑点。permission和requiredPrivateInfos是权限声明,凡是涉及相册、摄像头、位置的,声明缺失会导致真机调用直接失败,而开发者工具往往不报错。
第三个是云开发的初始化位置。搜索wx.cloud.init或者cloud.init,一般会在app.js和每个云函数入口里各出现一次:
// app.js App({ onLaunch() { if (!wx.cloud) { console.error('基础库版本过低,请升级') return } wx.cloud.init({ env: 'your-env-id', // 必须换成你自己环境的 ID traceUser: true // 在控制台记录访问用户,便于排查 }) } })env这个字段是必改项。作者的环境 ID 在你的账号下不存在,不改就是每次调用都返回环境不存在。traceUser打开之后,云开发控制台的用户管理里能看到调用记录,排查「到底有没有请求进来」时很有用。云函数里的cloud.init建议统一写成cloud.DYNAMIC_CURRENT_ENV,它表示自动使用当前所在环境,这样你以后开测试环境和生产环境时不用改代码。
2.3 把包跑起来的最小动作序列
配置看完,接下来按顺序做四件事:解压、扫一遍硬编码、导入、部署云函数。第一步用命令行做,比在图形界面里翻文件快得多。
# 解压到独立目录,避免和已有项目混在一起 unzip -q 人工智能实战微信小程序demo.zip -d ai-demo cd ai-demo # 找出所有可能的硬编码密钥与环境标识,这些是必须替换的 grep -rn "secret\|SecretKey\|apiKey\|api_key\|access_token\|env:" \ --include="*.js" --include="*.json" . | grep -v node_modules # 检查每个云函数目录是否完整(有 index.js 且 有 package.json) for d in cloudfunctions/*/; do [ -f "$d/index.js" ] && [ -f "$d/package.json" ] \ && echo "OK $d" || echo "MISS $d" done第一条grep的目的是把所有需要你替换的敏感配置一次性列出来,而不是等你运行到某一步报错了再回头找。输出里如果出现明显的密钥字符串,说明作者把配置直接写进了代码,你要做的是把它挪到云函数的环境变量里,而不是原地改一改继续用。第二条循环是在检查云函数目录的完整性,输出MISS的那个目录需要你手动补一个package.json,内容最少包含函数名和wx-server-sdk依赖。
接下来在开发者工具里导入项目,填入自己的 AppID,开通云开发并创建一个环境,把环境 ID 填回app.js。然后在云函数目录上逐个右键「上传并部署:云端安装依赖」。注意是「云端安装依赖」而不是「所有文件」,后者会把本地的node_modules一起传上去,几百兆的包传十分钟还经常中断。部署完成后在云开发控制台点一次「测试」,确认函数能返回结构化结果,再去点页面上的按钮。这个顺序能帮你把「前端问题」和「服务端问题」分开,省掉大量来回猜的时间。
3. 把 AI 能力接进小程序:一次图像识别请求的完整链路
骨架清楚了,接下来是最核心的一段:用户在页面上点一下,到结果出现在屏幕上,中间到底发生了什么。这段链路写得好不好,直接决定这个 demo 是「能演示」还是「能交付」。我见过太多包在这一段图省事,前端直接wx.request打到外部接口上,密钥明文躺在 js 里,压缩包里一搜就有。
3.1 前端直连的三道坎,决定了要走云函数中转
先说清楚为什么这类 demo 基本都选择云函数中转,而不是前端直接请求。
第一道坎是密钥。小程序的前端代码在打包后是可以被解出来的,任何写进 js 的密钥都等于公开。哪怕你做一层简单的混淆,也只是把被发现的时间从十分钟推迟到一小时。密钥必须待在服务端,这是硬约束。
第二道坎是域名白名单。小程序里用wx.request请求的域名必须提前在后台配置成合法域名,而且不能是 IP、不能带端口、必须是 https。开发期可以在工具里勾掉校验,但真机一定失败。而走wx.cloud.callFunction调用云函数不需要配置任何域名,云开发自己解决了这一层。
第三道坎是错误收敛。上游 AI 服务的错误码、限流策略、超时行为各不相同,把这些差异暴露给前端,页面里就会堆满 if-else。正确做法是在云函数里把上游结果统一成{ code, data, msg }三段式,前端只认这一种结构。
所以这个包里的正确调用路径应该是:页面 →wx.cloud.callFunction→ 云函数 → 外部 AI 服务,返回时逐层收敛。如果你拆开的包里发现某处是wx.request直连,那基本可以判定作者是为了省事跳过了中转层,这段代码在你自己项目里必须重写。
3.2 前端:从选图到拿到结果的完整代码
下面这段是页面里的核心逻辑,包含选图、读文件、体积拦截、调用、错误处理五个环节。
// pages/vision/vision.js Page({ data: { loading: false, result: null, errMsg: '' }, async onPick() { // 1. 选图:sizeType 只保留 compressed,原图很容易把 base64 撑爆 const media = await wx.chooseMedia({ count: 1, mediaType: ['image'], sizeType: ['compressed'], sourceType: ['camera', 'album'] }) const filePath = media.tempFiles[0].tempFilePath // 2. 读成 base64,encoding 必须显式指定,否则拿到的是 ArrayBuffer const base64 = await new Promise((resolve, reject) => { wx.getFileSystemManager().readFile({ filePath, encoding: 'base64', success: r => resolve(r.data), fail: reject }) }) // 3. 体积闸门:超限就直接拦,不要等云函数超时再报错 if (base64.length > 800 * 1024) { this.setData({ errMsg: '图片过大,请裁剪后重试' }) return } // 4. 调用云函数,name 必须和 cloudfunctions 下的目录名完全一致 this.setData({ loading: true, errMsg: '' }) try { const res = await wx.cloud.callFunction({ name: 'aiVision', data: { image: base64, scene: 'general' } }) const payload = res.result || {} if (payload.code !== 0) { this.setData({ loading: false, errMsg: payload.msg || '识别失败' }) return } this.setData({ loading: false, result: payload.data }) } catch (e) { // 云函数调用失败时 errMsg 才是给人看的,message 经常是空的 this.setData({ loading: false, errMsg: e.errMsg || '网络异常' }) } } })几个参数值得单独说。sizeType: ['compressed']表示只要压缩后的图,但这里的「压缩」是平台给的默认档位,你控制不了具体尺寸,所以后面还要自己做一次二次压缩,这一点在第 4 章展开。encoding: 'base64'不能省,很多新手直接readFile拿到ArrayBuffer就往云函数传,序列化之后体积更大而且上游解析不了。第三步的体积判断用的是字符串长度,base64 编码后体积约为原图的 4/3,所以 800KB 的字符长度大致对应 600KB 的原始字节,这个阈值和云函数事件体的上限之间留了余量。最后name字段是最常见的低级错误来源:目录叫aiVision,这里写成aivision,报错信息是「云函数不存在」,但实际原因只是大小写。
3.3 云函数:把上游差异收敛在一个文件里
云函数这一层要做三件事:校验入参、调用上游、把结果规范化。下面是一个可以直接套用的骨架。
// cloudfunctions/aiVision/index.js const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) // 上游调用单独抽出来,换供应商时只改这一个函数 async function callUpstream(buffer, scene) { // 这里换成实际的上游调用,密钥从环境变量读,不要写死 const apiKey = process.env.AI_API_KEY if (!apiKey) throw new Error('AI_API_KEY not configured') // ... 发起请求,返回原始结果 } // 把不同供应商的返回结构统一成前端认识的样子 function normalize(raw) { return { labels: (raw.labels || []).map(i => ({ name: i.name, score: i.score })), top: raw.labels && raw.labels.length ? raw.labels[0].name : '' } } exports.main = async (event) => { const { image, scene = 'general' } = event || {} if (!image) return { code: 400, msg: '缺少图片数据' } const buffer = Buffer.from(image, 'base64') if (buffer.length > 600 * 1024) { return { code: 413, msg: '图片超出体积上限' } } try { const raw = await callUpstream(buffer, scene) return { code: 0, data: normalize(raw), msg: 'ok' } } catch (e) { // 只打错误码和信息,不要把完整报文和密钥打出来 console.error('vision failed:', e.code || e.name, e.message) return { code: 500, msg: '识别服务暂时不可用' } } }cloud.DYNAMIC_CURRENT_ENV的作用前面提过,它让函数跟随当前环境,不用每次改环境 ID。Buffer.from(image, 'base64')这一步是把前端传来的字符串还原成二进制,还原之后再算体积才是真实的字节数,比前端用字符长度估算更准。体积上限设成 600KB 是经验值,云函数的事件参数有体积限制,留出余量比压着上限跑更稳。normalize这个函数是整个文件里最值得保留的设计:它把上游返回的字段名、层级、分数范围全部翻译成前端自己的格式,以后换一家服务商,前端一行都不用改。最后的catch里只打错误码和消息,不打完整报文,是因为云函数日志在控制台里是明文可见的,把上游响应整个打出来,等于把可能存在的鉴权信息也留了一份。
3.4 返回结构与渲染状态机
前端拿到{ code, data, msg }之后,页面状态其实只有四种:空闲、加载中、成功、失败。把这四种状态用data里的三个字段显式表达出来,页面就不会出现「转圈转到天荒地老」或者「结果和错误提示同时在屏幕上」这种尴尬。
具体做法是在wxss里给三种状态各准备一个容器,用wx:if和wx:elif控制显示,而不是靠控制透明度或者display去藏。原因很实际:藏起来的节点依然在渲染树里,图片类的结果藏起来照样占内存,反复识别几次之后真机上就开始卡。另一个细节是加载态一定要给一个可取消的出口,比如三秒后还没返回就把文案从「识别中」改成「仍在处理,请稍候」,用户不会以为程序卡死而反复点按钮——重复点击是这个 demo 在演示现场最常见的翻车方式,一次点了五次,五次请求全部打到上游,限流直接触发。
4. 参数怎么调:压缩、阈值、并发这三组数决定体验
链路通了之后,真正决定这个 demo 好不好用的是几个数字。同样是接一个识别服务,有人做出来点一下等八秒、十次里错三次,有人做出来一秒出结果、错了还有兜底文案,差别几乎全在参数上。这一章把最需要调的几组参数拆开讲。
4.1 图片压缩:先把 base64 的体积账算清楚
选图接口自带的compressed只保证「压过」,压到多少完全看平台实现,实测下来几百 K 到两三兆都出现过。而 base64 编码会让体积再涨三分之一,一张 2MB 的图编码完接近 2.7MB,直接超过云函数事件体上限。所以第二次压缩是必需的。
// utils/image.js // 用离屏 canvas 做二次压缩,把长边限制在 1024 以内 function compressByCanvas(filePath, { maxEdge = 1024, quality = 0.8 } = {}) { return new Promise((resolve, reject) => { wx.getImageInfo({ src: filePath, success: (info) => { // 只缩不放,避免小图被拉伸后变模糊 const scale = Math.min(1, maxEdge / Math.max(info.width, info.height)) const w = Math.round(info.width * scale) const h = Math.round(info.height * scale) const canvas = wx.createOffscreenCanvas({ type: '2d', width: w, height: h }) const ctx = canvas.getContext('2d') const img = canvas.createImage() img.onload = () => { ctx.drawImage(img, 0, 0, w, h) wx.canvasToTempFilePath({ canvas, fileType: 'jpg', // 用 jpg,png 是无损格式压不下去 quality, // 0~1,0.7~0.85 是通用区间 destWidth: w, destHeight: h, success: r => resolve(r.tempFilePath), fail: reject }) } img.onerror = reject img.src = filePath }, fail: reject }) }) }maxEdge设成 1024 是个折中:识别类任务里,长边超过一千像素之后准确率提升非常有限,但传输和推理时间几乎线性上涨。quality给 0.8,低于 0.7 时文字类图片的边缘会糊,OCR 场景下错字率明显上升;高于 0.9 体积又压不下来,等于白做。fileType必须用 jpg,png 是无损压缩格式,quality参数对它基本无效。这里有一个容易被忽略的点:wx.createOffscreenCanvas和canvasToTempFilePath的组合在不同基础库版本上参数支持不完全一致,导入别人的包之后先在真机上跑一次压图,把输出的临时文件大小打出来看一眼,比对着文档猜要快。
4.2 置信度阈值与「模型也不确定」的兜底
识别结果一般带分数,页面直接拿最高分那一项展示是最省事的做法,也是最容易在演示现场翻车的做法。分数 0.35 的结果和一个 0.92 的结果,用同样的口吻展示给用户,前者就是在制造错误信息。
比较稳的处理是设两档阈值。低于低档的直接不展示,告诉用户「没认出来,换一张试试」;处在两档之间的展示出来,但加一句「可能是」。这个逻辑放在前端还是云函数,取决于你是否要在别的地方复用——如果只有这一个页面用,放前端就够;如果有多个入口,就放进云函数的normalize里,让所有调用方拿到一致的判断结果。阈值具体给多少,取决于场景:通用物体识别我一般用 0.6 和 0.85 两档,文字识别因为本身准确率高,可以用 0.5 和 0.75。这些数不需要精确,需要的是有一个明确的、写死在配置里的值,而不是散落在各处的魔法数字。
还有一个细节:识别不出结果和请求失败要区分开。前者应该给「换一张更清晰的图」这类引导,后者应该给「稍后重试」,两句文案混用会让用户完全不知道该干什么。
4.3 并发、节流与结果缓存
演示类 demo 最容易忽略的就是并发。用户手快,连点五下,五个请求同时飞出去,上游按秒计费的接口可能直接触发限流,后面所有请求一起失败。
| 参数 | 常见默认 | 建议值 | 影响 |
|---|---|---|---|
| 单次识别超时 | 未设置 | 8 至 12 秒 | 太短误判失败,太长用户流失 |
| 按钮点击间隔 | 无限制 | 800 毫秒 | 防止连点造成并发 |
| 同时请求数 | 无限制 | 1 | 前端串行,简单可靠 |
| 结果缓存条数 | 无 | 最近 10 条 | 回看历史不重复请求 |
实现上最简单的方式是给按钮加一个disabled状态,在请求返回前不允许再次触发。比这更稳一层的是记录上一次请求的开始时间,间隔不足就忽略本次点击——因为setData是异步的,disabled生效有延迟,纯靠它拦不住极端连点。缓存则用wx.setStorageSync按图片的临时路径做键,存最近十条结果,用户来回切换时不用重新请求。这些做法都不复杂,但它们决定了这个包在真实网络下是「能用」还是「勉强能演示」。
4.4 语音与文本类能力的参数差异
如果这个包里不止图像识别,还带了语音或文本类功能,参数的关注点要换一套。语音识别的关键参数是采样率和单次时长:采样率统一到 16k,单次录音控制在 60 秒以内,超长音频必须先切片再分段请求,否则一次请求大概率超时。文本类功能的关键参数是长度上限和截断策略,输入超过模型上下文长度时,是从头截、从尾截还是分段处理,结果差别很大,必须在云函数里明确写死一种,别指望上游帮你处理。
还有一点常被忽略:语音和文本的返回结构通常比图像更复杂,嵌套层级更深。normalize函数在这里的价值就体现出来了,把嵌套结构拍平成前端能直接渲染的数组,页面里就不会出现三层wx:for套着写的场面。
5. 避坑与排查:上线前必须过一遍的五条
这一段是这个包里最值钱的部分。demo 在开发者工具里跑通,和它在真机上、在别人手机上跑通,中间隔着好几道坎。下面五条是我在这类项目里反复踩到的,每条按现象、原因、解决三段写。
5.1 合法域名:工具里一切正常,真机报 url not in domain list
现象是开发者工具里点按钮一切正常,用手机扫码预览,点下去直接弹「url not in domain list」,或者干脆无响应。
原因是工具里默认勾选了「不校验合法域名、web-view 域名、TLS 版本以及 HTTPS 证书」,勾着的时候任何请求都能出去,真机不认这个设置。这个包如果有一处wx.request直连外部接口,就一定会在这一步翻车。
解决办法是先自己复现:在工具的详情设置里把「不校验合法域名」取消勾选,再点一次按钮,能立刻复现的就不用等到真机。然后分两条路走,一条是去后台把域名配进 request 合法域名列表,必须 https,不能是 IP,不能带端口;另一条是把这处直连改造成走云函数,彻底绕开域名配置。后一条更彻底,我个人优先选它。
5.2 云函数超时与冷启动:第一次点永远失败
现象是刚部署完或者隔了半小时没操作,第一次点击失败,第二次开始就正常。
原因有两个。一是冷启动,云函数在一段时间无调用后会被回收,下一次调用需要重新拉起实例,这段时间上游请求很容易撞上默认超时。二是超时时间配置偏短,默认值跑普通查询够用,跑一次带模型推理的请求就不够了。
解决办法分三步。第一步去云开发控制台把该函数的超时时间调大,AI 类接口给到十几秒是合理范围,别用默认值硬扛。第二步在前端加一次自动重试,只对超时类错误重试一次,失败两次再报错给用户,重试间隔给 500 毫秒左右。第三步是在页面首次onLoad时发一个空请求预热,把冷启动的成本挪到用户还没操作的时候。这三步做完,第一次点击失败的观感基本消失。
5.3 真机与工具的渲染差异:结果有了,屏幕是空的
现象是控制台里setData的数据明明是对的,页面上什么都没显示。
原因通常是样式问题而不是数据问题。这类 AI 演示页很喜欢用绝对定位叠在图片上画框、标点,在工具里正常,到 iOS 上因为canvas或image的尺寸计算方式不同,标框整体偏移或者被裁掉。另一种情况是用了position: fixed弹层,在部分安卓机上被键盘顶上去之后就再也回不来。
解决办法是先在页面上加一个纯文本的调试区,把JSON.stringify(result)打到屏幕上,确认数据确实到了视图层,再去查样式。确认是样式问题之后,标框类的渲染改成基于图片实际渲染尺寸按比例换算,而不是写死像素值;弹层改成绝对定位配合页面滚动容器,不要用 fixed。这类问题没有通用解法,只能靠一次真机验证把它逼出来,所以别等到演示当天才第一次上手机。
5.4 base64 体积超限:报错信息完全看不懂
现象是换了一张高清照片就失败,错误信息里出现 payload 过大、参数超限之类看不懂的词,换成小图又好了。
原因是原图读成 base64 之后体积膨胀,超出了云函数事件参数的上限。前端那句体积判断如果没写,或者阈值设得过大,就会一路放行到云函数才炸,报错自然难懂。
解决办法是把闸门放在前端,压图之后立刻算体积,超过阈值直接提示用户裁剪,不要让请求发出去。同时云函数里再兜一层,对还原后的Buffer长度做二次校验,返回明确的错误码。两层都做,前端那一层保证用户体验,云函数那一层保证不会因为有人绕过前端直接调用而把费用打上去。
5.5 授权与隐私:功能都对,就是提交不过
现象是功能层面全部正常,提审被驳回,理由指向用户信息收集和权限声明。
原因是这类 demo 为了演示方便,往往在onLaunch里就把能拿的权限全申请一遍,或者在没有实际使用场景的页面上弹授权框。审核看的是「是否必要」,不是「是否能实现」。
解决办法是把权限申请挪到真正用到的那一刻,选图之前再申请相册,录音之前再申请麦克风,不要提前批量申请。涉及用户信息的地方,页面上要有明确的用途说明,不需要长篇大论,一句话说清楚拿这个信息做什么就够。这条看似和代码无关,但它决定这个包能不能真的上线,值得在改完功能之后专门过一遍。
6. 交付前的回归:用一组固定图把整条链路验一遍
功能改完、参数调完,最后一件事是给自己留一个可重复执行的验证方法。AI 类应用有个特点:输出是不确定的,同一张图两次调用结果可能略有差异,所以不能用「结果是否等于某个值」来做断言,否则测试永远在失败。我的做法是把验证拆成两层,一层验结构,一层验体验。
结构层用一个固定的小脚本,在云函数本地调试时跑,检查返回字段是否齐全、类型是否正确、分数是否在 0 到 1 之间。这部分是确定性的,可以写成断言。
// scripts/check-shape.js // 只校验结构,不校验内容,避免被 AI 输出的随机性干扰 const cases = [ { name: 'empty', event: {} }, { name: 'tiny', event: { image: 'AAAA' } }, { name: 'normal', event: { image: require('./fixtures/sample.json').b64 } } ] ;(async () => { const { main } = require('../cloudfunctions/aiVision/index.js') for (const c of cases) { const r = await main(c.event) const ok = typeof r.code === 'number' && typeof r.msg === 'string' console.log(c.name, ok ? 'PASS' : 'FAIL', 'code=', r.code) if (r.code === 0) { // 成功分支必须带 data,且 data 里至少有一个数组 const hasArray = Object.values(r.data || {}).some(Array.isArray) console.log(' data shape:', hasArray ? 'PASS' : 'FAIL') } } })()这个脚本的价值不在于覆盖率,而在于它能在你每次改完normalize之后三十秒内告诉你有没有把某个分支的返回结构改坏。fixtures/sample.json里存一张压到很小、base64 写进文件的样例图,保证离线也能跑。
体验层没法自动断言,只能靠一张固定的回归清单手动走。
| 检查项 | 通过标准 |
|---|---|
| 冷启动首次识别 | 不报错,或自动重试后成功 |
| 连点五下按钮 | 实际只发出一次请求 |
| 超大原图 | 前端拦截并给出可操作提示 |
| 断网后点击 | 提示明确,不白屏不卡死 |
| 识别失败结果 | 有「换一张」引导,不是空白 |
清单不用长,五条足够,关键每次改完都走一遍。
最后说一个我自己的教训。早些年接手一个类似的小程序包,功能演示很顺利,我直接在上面改业务,改到第三天发现上游接口偶发返回空数组,页面空白,用户以为卡住了。回头查才发现原来的normalize里对空结果没有任何兜底,作者本地测试用的图片都是清晰大图,从来没触发过。从那以后我每次拆这类带 AI 能力的包,都先把「空结果、超时、超限」这三种失败路径各手动构造一次,看着页面到底显示什么,再动手改一行业务代码。希望帮到你。
本文还有配套的精品资源,点击获取