前言|卡在"意"字这一步
"一心一意。"
下一条,要从"意"字开始。
盯着最后那个字,脑子里先冒出来的却是"义无反顾"。读音像,字不对。再想一会儿,好不容易想起"意气风发",又忍不住怀疑:这个词刚才是不是在提示里见过?
我把这种小小的卡壳做成了一个 AIUI 智能体,名字叫"接着说"。它不追求一口气接几百条,一局只有五轮。能自己想到就接,想不到就借一点线索;接完再回头看看,这五条里,哪些是自己想起的,哪些靠了候选,哪些已经看过答案。

这局游戏得先回答几个小问题:上一条是什么,下一个字是什么,刚才看过哪层提示。尤其到了复盘,不能只看最后按的是哪个按钮;如果已经看过答案,再亲口说出四个字,也得留下看过答案的记录。
给 Rokid 眼镜做这局游戏,我把空间优先留给四个字和下一步动作。当前词、接续字、提示、复盘各有自己的阶段,打开哪一步就看哪一步,规则说明需要时再展开。

先交代实际范围:下面的运行画面来自 AIUI Studio 的 Glasses/Glasses 2 模拟目标,原生画布是 480 × 352。文中的三段操作 GIF 都由真实连续画面生成。项目完成了本地逻辑检查、平台包检查和模拟器操作验证;页内收音在当前浏览器环境连接失败,我会把这段失败和处理方式一起写出来。本文没有眼镜真机体验,也没有把提审状态写成已经上架。
1、先玩一局:五轮的规则与节奏
接龙规则只有三条:简体四字,首尾原字相接,当局不能重复。同音字不能顶替,开局给出的词也算已经出现过。
"原字"这个约定要早说。否则屏幕上要求"意",用户说了"义",系统只回一句"错误",用户并不知道是识别错了、词不对,还是游戏另有规则。我把最后一个字单独框出,并在它下面直接写"下一条,从'意'字接起"。玩法页再解释完整规则,正常游戏不用一直翻说明。

操作上保留两条路。页内有"说一条成语"的入口,也有"按键接词"的入口。后者提供三条候选,里面恰好一条能接上。它让缺少可用麦克风的环境也能走完玩法,不过只要看过候选,这一轮就会留下"看过候选"的记录,不能再冒充独立回答。
卡住时还可以选"给点提示"。先给意思,再露出前两个字,最后才给完整答案。这个顺序不是让玩家多按几次,而是保留一点继续想的余地。很多时候,"我知道有这么个词"和"我现在能把它说出来"之间,恰好就差一点线索。

每接上一条,页面先停在反馈阶段:放大新词,显示短释义,说明这一条借助了什么帮助,以及记录已经保存。玩家主动选"继续下一轮",才进入下一次接词。五轮结束后,不给一个含糊的"你真棒",而是列出四种来源的数量,再允许逐条翻看。
这里的复盘是回看,不是测评。"独立接了几条"只描述本局的操作记录,不能据此评价成语水平。候选和提示也不是扣分项目,它们本来就是游戏的一部分。界面要如实记住发生过的事,又不必把一次轻松接龙做成考试。
2、建立工程:页面入口放在哪里
复现时先在 AIUI Studio 新建"接着说"。左上角菜单提供本地导入和AIX导入等入口;工程目录里要能直接找到app.json、app.js和pages。建好以后再填名称与图标,后续核对包信息时就有明确的对应对象。

读者复现时可以先从文末仓库取得代码,再通过"本地导入"选择工程目录;如果手里已经有 AIX,使用对应的 AIX 导入入口。这里有个实际细节:菜单写的是 GitHub 导入,本文的源码在 GitCode,不能直接把 GitCode 地址当成 GitHub 导入地址去填。用本地目录导入更直观。
导入后到平台基本信息页核对名称、图标和Agent ID,再检查页面路径。界面上"真机调试"是栏目名称,本文的画面仍来自模拟器,不能据此当作已经连接眼镜。

工程没有网页开发服务器,也不靠 npm start 启动。运行入口由 AIUI 读取,页面文件是 Ink,游戏规则和存储则拆在普通 JavaScript 模块里。源码目录按职责分成下面几部分:
bash
idiom-relay/
├── AGENTS.md 宿主理解意图时遵循的约定
├── app.js 应用入口
├── app.json 名称、图标、页面与权限
├── assets/icon.svg
├── assets/icon.png
├── modules/
│ ├── lexicon.js 词条、释义、可接续词与选词
│ ├── game.js 规则、轮次、帮助来源和状态校验
│ ├── storage.js OPFS 文件读写与回读
│ └── input.js 按键归一与语音生命周期
├── pages/relay/relay.ink 页面、动作与显示
└── tests/verify.mjs 可独立运行的逻辑检查
app.js 的入口保持很小,不在启动时打开麦克风,不把读取记录塞进一个无法结束的同步等待。真正读取一局数据的动作交给页面生命周期。当前实际入口如下:
javascript
export default { onLaunch() {}, globalData: {} };
app.json 里需要特别核对 pages。这个工程只有一个页面,路径是 pages/relay/relay,没有 .ink 后缀。agentLogo 指向项目图标,RECORD_AUDIO 对应语音入口。清单声明的最低引擎范围是 >=0.17.0;这只是工程声明,实际兼容情况仍需要在所用宿主中验证。

如果导入后没有正确页面,先检查工程根目录、清单路径和页面定义,再去排查按钮逻辑。入口文件放错层级,或者把页面路径拼错,都可能让人误以为是游戏卡住了。把"宿主能找到页面"和"页面内动作正确"分开看,排障会快很多。
3、开局、接词、求助怎样分流
AIUI 里的这类项目有两个容易混在一起的环节:宿主要先理解用户想做什么,页面才根据自己的状态执行。比如"玩一局成语接龙""我接意气风发""再给一点提示",分别对应开局、接词和求助,不应该都当成一条待提交的成语。
我在 AGENTS.md 里把这些约定写得很直接。只想打开游戏时用 action=open;明确要开始时才用 start;接词用 reply,并把用户说的词放进 word。尤其不能让模型先替用户改成一个能接上的词,再把修改后的词交给页面。那样看起来次次都答对,玩法实际上已经变了。

同一组动作也放进 relay.ink 顶部的页面定义。action 限定可用动作,word 保留接词原话,seed 用于指定开头,from 标记入口来源。这里没有另外藏一份更宽松的按钮规则:按钮、页内语音结果、宿主页面参数,最终都要走游戏控制器。

几个常用表达可以按下面的关系理解:
| 用户想做的事 | 传入页面的动作 | 页面负责的处理 |
|---|---|---|
| 打开、继续已有游戏 | open | 读取记录,回到实际阶段 |
| 明确开始一局 | start | 校验开局,必要时先确认替换 |
| 接一条成语 | reply + word | 规则判定、记录帮助来源、保存 |
| 给点提示、看候选 | hint / choices | 保存已经看过的帮助 |
| 结束、回看 | finish / review | 结束先确认,复盘按保存内容显示 |
这组参数描述的是接口约定,不等于每种自然语言都已经完成语音实测。我验证了参数和动作逻辑;真实收音链路的环境限制放在后文。文章里的按钮演示也不会因为经过了同一控制器,就被写成"语音识别成功"。

这样拆分后,改提示文案不会影响成语判定,扩充词库也不用到页面按钮里逐处找词。页面不再负责"猜用户说的是哪个成语",游戏模块也不必知道这条词来自语音还是按键。每个部分都只处理自己能明确负责的事情。
4、91条词库与后续分支
这一版内置 91 条简体四字词条,配短释义,释义由项目自行编写。数量不大,正好能先把规则和交互做扎实。没有联网查词,也没有让模型在每一轮临时生成一条"可能正确"的词。
词库是一个数组,初始化后生成按词检索的 Map。首字、尾字和可接续词的逻辑都集中在 lexicon.js。查找下一条时,先比较原字,再排除这一局已经出现过的词,开局词也在排除范围内。
javascript
export const LEXICON = rows.map(([word,meaning])=>({word,meaning}));
export const WORDS = LEXICON.map(v=>v.word);
export const SEEDS = ['一心一意','胸有成竹','马到成功'];
const byWord = new Map(LEXICON.map(v=>[v.word,v]));
export const entry = word=>byWord.get(word)||null;
export const head = word=>Array.from(word||'')[0]||'';
export const tail = word=>Array.from(word||'').slice(-1)[0]||'';
export function candidates(word,used=[]){
const seen=new Set(used);
return WORDS.filter(v=>head(v)===tail(word)&&!seen.has(v));
}
如果只做"找到第一条能接的就给它",很容易出现一种不太愉快的体验:提示看上去没问题,玩家接上后,下一轮却立刻没词。为此 chooseNext 会在有限深度内比较后续分支,优先选择还可以往下接的词。搜索深度最多看四步,既给五轮玩法留余地,也不给一个小词库加上无上限搜索。
这不是保证所有路径都能接满五轮。玩家可能走进词库边缘,分支也可能因为已经用过的词而变短。到边界时应该结束本局、保留已接记录,并明确说"这条分支暂时没有库存",不能让玩家替题库的有限容量背锅。
三个常用开局分别是"一心一意""胸有成竹"和"马到成功"。主页的"换个开头"只切换待开始的词,真正按"开始五轮"才保存新局。把选择和开局分开,可以避免玩家只是翻看一个开头,就把原有记录改掉。

以"马到成功"开始时,接词页会强调尾字"功"。从这里可以接"功成名就",再往"就事论事""事半功倍"等方向走。换的是词和候选,页面仍然沿用同一套布局与动作,不为每个开局再写一个单独分支。

还有一个边界要说准确:本地没收录,不等于词不存在。"意气洋洋"没有放进这 91 条里,但在教育部《成语典》附录能找到对应的"意氣洋洋"。因此遇到这种输入,页面只说"本题库暂未收录",不下"这不是成语"的结论。有限词库的游戏,更要知道自己的判断能到哪里。
4.1、扩充词库时一起补什么
扩充这个工程时,我建议先加一条完整分支,再跑一次检查。比如新增一条以"意"开头的词,要同时想一想它的末字能往哪里走,已有哪条路径能够到达它,它会不会很快绕回本局已经出现的词。单词数量增加了,能玩的路径却不一定随之变长。
短释义也要跟着词一起维护。第一层提示依赖它,接上反馈和逐词复盘也会再次使用它。把释义写成答案本身,或者在解释里直接包含整个词,会让第一层就相当于揭晓。读者可以从自己熟悉的词开始补,查清词义后再用短句改写,别只凭四个字看起来顺口就入库。
最后检查两件事:新词不能重复,字形要符合这一版的简体原字约定。繁体、异体或同音词如果混进来,虽然肉眼觉得差不多,程序比较的仍然是原字。未来真要支持更多接龙方式,应该增加明确的玩法和对应检查,而不是在 head、tail 里悄悄替换几个字。
5、接不上时,怎样解释原因
game.js 的 judge 是所有接词输入共同经过的入口。它返回结果码和能直接显示的短说明,页面不用再拼一套"差不多"的判定。完整顺序是游戏阶段、四字形态、当局重复、首尾原字、题库收录。
arduino
export function judge(session,value){
const word=normalizeWord(value);
if(!session||session.phase!=='playing')return {ok:false,code:'phase',word,message:'先开始一局,或按继续进入下一轮'};
if(!/^[\u3400-\u9fff]{4}$/u.test(word))return {ok:false,code:'shape',word,message:'一次只接一个简体四字成语'};
if(usedWords(session).includes(word))return {ok:false,code:'repeat',word,message:'这局已经用过"'+word+'",换一条'};
const required=tail(currentWord(session));
if(head(word)!==required)return {ok:false,code:'head',word,message:'要从"'+required+'"字接起;同音不同字不算'};
if(!entry(word))return {ok:false,code:'unknown',word,message:'本题库暂未收录"'+word+'",未判定它不是成语'};
return {ok:true,code:'ok',word,message:'接上了'};
}
顺序会影响用户看到的结果。还没开局时,应该先引导开始,而不是纠结他输入几个字。在反馈页重复确认时,应该说明先继续进入下一轮。把开局词再说一遍,则优先提示"这局已经用过",比只报一个首字不符更贴近这次错误。

输入归一也刻意收得比较窄。可以接受"我接意气风发"、引号和句末标点,方便自然表达;不会从一大段话里随便挑四个字,更不会把"义"自动改成"意"。繁简转换、同音接龙都不属于当前规则,宁可把边界说清楚,也不要让一次"聪明纠错"悄悄改变游戏。
模拟器里选了不符合首字要求的候选后,页面仍留在当前轮,计数没有增加,并直接提示需要的字。这里值得看的是错误前后数据有没有变化,而不只是红字或提示有没有出现。失败输入不应该消耗一轮,也不应该重置玩家已经看过的提示。

另一层校验发生在存档恢复时。程序会从开局词重新核对每条已接记录:词必须存在,首尾要衔接,不能重复,帮助类型和提示层级也要一致。只检查 JSON 能不能解析还不够;一份能解析却不连贯的记录,同样不能直接拿来继续游戏。
6、提示分三层,记录从第一层开始
"一心一意"的下一条提示目标是"意气风发"。第一次求助只给"精神振奋,做事很有朝气。"这句话能帮人回忆意思,却没有直接把答案写出来。玩家仍可以回到接词页,再想一想。

有时候读完释义的状态,大概就是下面这样。意思懂了,那个词还卡在嘴边。

第二层才露出"意气□□",第三层展示完整词。三层必须指向同一条成语,否则第一层给甲的释义、第二层却露出乙的字,线索越多越容易误导。所以第一次求助就保存 hintTarget,之后只增加 hintLevel,不会每点一下重新抽一个答案。
ini
export function hintView(session){
if(!session||!session.hintLevel)return {level:0,title:'还没使用提示',text:''};
const target=entry(session.hintTarget),level=session.hintLevel;
return {level,title:level===1?'线索一 · 看意思':level===2?'线索二 · 补后两字':'线索三 · 已揭晓',
text:level===1?target.meaning:level===2?target.word.slice(0,2)+'□□':target.word};
}
下面把三层提示和最终确认连着走一遍。要留意顶部计数:即使完整答案已经出现,仍是已接零条;主动选"用这条继续"后,才进入接上的反馈。这一停顿留给玩家做决定,也让复盘记录有明确的提交时刻。

提示不是临时弹出的装饰,它也属于本轮状态。只要已经看过,就需要保存。第三层揭晓后重新运行,页面会回到这一轮的接词状态,提示记录还在,再次进入提示仍是已经揭晓的同一条;程序不会趁恢复时把它自动接上,也不会把帮助痕迹清空。

为什么连"只看一下"也要保存?因为帮助已经改变了这次回答的条件。假设用户先看见"意气风发",退出提示,再亲口说出这四个字,系统如果仅看提交方式,就会把它记成独立回答。记录真实的帮助过程,比事后猜测答案从哪里来要稳妥。
7、候选答案怎样进入复盘
候选页的目的很朴素:不用键盘和可用麦克风,也能完成一次接龙操作。三条里放一条可接续词,另外两条也来自词库,但首字不符合当前要求。这样考察的是能否看清衔接,而不是让玩家在三个胡编的词里找真成语。

正确项的位置会随局编号和轮次变化,不能一直放在同一个位置。下面换成"马到成功"开局,候选也随当前尾字重新生成。不同开局不共用一组写死的按钮文字,已使用过的词也会被排除。

候选一旦显示,choiceShown 就成为这一轮的事实。用户选错、返回,再换一种方式作答,都不会把它恢复成"从没看过"。同一轮重复打开候选,会取回已经生成的那组,不会通过反复开关刷新出新答案。
提交时再把帮助来源压成一项记录,优先级是"完整揭晓、借助提示、看过候选、独立回答"。这里的高低只表示信息暴露程度。既看过候选又看过第二层提示,记录为借助提示;已经看过完整词,之后再从候选选中,也仍是揭晓后继续。
php
const help=s.hintLevel===3?'reveal':s.hintLevel>0?'hint':s.choiceShown?'choice':'independent';
s.rounds.push({word:checked.word,help,hint:s.hintLevel});s.phase='accepted';s.hintLevel=0;s.hintTarget='';s.choiceShown=false;s.offer=null;
候选提交还携带一个与局、轮次和记录版本相关的 token。页面旧选项如果迟到,不能直接作用在新的一轮。处理这种情况时,程序要求重新打开候选,而不是凭"词看起来正确"就接受。小项目也会碰到旧界面、重复点击和迟到回调,提前把动作绑到当前状态,比出错后补一串按钮禁用更清楚。

注意反馈页里的"看过候选 · 这一条已保存"。它把两个不同问题放在一起回答:这一条怎样得到,以及操作有没有真正落下去。下一轮开始时,只清理新一轮的临时提示和候选,上一条已经记录的来源不会跟着清空。
8、阶段切换和镜腿焦点
这张 480 × 352 的画布没有很多可以浪费的位置。我把当前成语拆成四块,末字加框,上面放已接数量和五段进度条,下面只保留当前阶段需要的动作。玩家接词时看不到整套复盘操作,复盘时也不需要把收音按钮挤在旁边。
看官方产品页里的触控操作展示,会更容易理解为什么我没有照搬手机上密密麻麻的一排小按钮。本文用 Studio 的镜腿操作区验证焦点流程:滑动切换、单击确认、双击返回。它们在模拟器里是可点击的控制按钮,实际宿主的手感和语音表现仍要另测。

焦点不是靠按钮变亮一点点来暗示。当前动作同时有更粗的外框和 > 标记,亮度、字号也会变化。进入一个新阶段时,焦点回到第一个动作;逐词复盘只换词时则保留操作位置,否则每翻一条都被送回"上一条",很容易形成来回折腾。
按键事件统一经过 input.js。同向滑动的成对事件在 240 毫秒内去重,切换焦点后还有 120 毫秒的确认保护,避免同一轮手势顺带确认刚刚滑到的按钮。这些数字是本项目的输入策略,不能理解成所有设备通用的最佳参数;后续有真机,仍应结合实际事件节奏调整。
页面阶段和存档阶段也需要分开。存档只保留正在接词、已经接上和已结束等必要状态;候选页、提示页、确认页是显示模式。这样重新运行后能回到确定的业务阶段,不需要把每个临时面板的开关都存进文件,再处理一堆恢复组合。

更要留意两个会改变一整局的动作:另开一局,以及结束本局。它们都先到确认页。在确认页中,新的语音动作也不能绕过当前决定,必须先用页面确认或取消。否则用户本来正在犹豫,一句别的话被理解成开局,旧进度就被换走了。

我补录了一段完整过程:从已经接上一条的局进入换局,先选"继续原局",页面仍回到"功成名就";再申请换局并确认,新局才开始;回到"看上局",还能看到旧局这条词和"看过候选"的记录。取消、确认、旧局留存,三个结果在同一段连续操作里都能看见。

旧局并不是无限历史列表。当前文件只保留当前局和最近一次结束的局,新的一次归档会替换更早的那份。这是这版的明确取舍:先做好一局与上一局的恢复,不把它包装成完整学习档案。用户知道会留下什么,也知道哪些内容不会一直保存。

9、局记录如何保存与恢复
如果点完按钮就先把页面改成接上,再在后台慢慢写文件,正常情况下看不出区别,写入失败时却会很尴尬:屏幕说完成了,重新进入又少一条。这次所有会改变记录的操作,都要先完成保存,页面才能进入成功反馈。
持久化使用 Agent 私有的 OPFS 文件 idiom-relay-session-v1.json。官方文档说明,可写流要在 close() 后才提交到文件后端;读取则通过文件快照的 arrayBuffer() 获得内容。我按这条接口约定实现读写,并在关闭之后额外回读,核对实际内容。AIUI 官方 OPFS 文档
先看数据形状。下面是使用实际控制器完成"开局、看一次提示、提交意气风发"后生成的结构示例。页面上短短一行"借助提示",背后保存的是明确的 help 和 hint,不是下次打开时再根据文案推测。
json
{
"schema": 1,
"revision": 3,
"serial": 1,
"current": {
"id": 1,
"seed": "一心一意",
"rounds": [
{
"word": "意气风发",
"help": "hint",
"hint": 1
}
],
"phase": "accepted",
"hintLevel": 0,
"hintTarget": "",
"choiceShown": false,
"offer": null,
"endReason": ""
},
"archive": null
}
外层的 schema 表示格式版本,revision 随已经提交的改变递增,serial 用来分配局编号。current 是当前局,archive 保存最近一次归档。已接词放在 rounds 中,每条只保留词、来源和提示级别;释义始终从词库读取,避免同一份释义在多个地方各改各的。
这个示例中 phase 是 accepted,说明词已经接上,但玩家还没按"继续下一轮"。本轮临时的 hintLevel 已经归零,不代表历史提示被抹掉;历史信息已经进入 rounds[0]。分清本轮临时状态与已提交记录,恢复时才不会把上一轮的提示带给下一条,也不会把上一条的帮助来源弄丢。

游戏控制器写入前先重新读取磁盘状态。如果磁盘内容已经与当前内存不同,就要求重新读取,避免继续用旧状态覆盖别人已经写过的内容。之后才复制内存状态、执行这次改变、递增版本、校验整份记录,再交给文件模块保存。内存里的正式状态最后更新。
文件层还有一个容易忽略的动作:短内容覆盖长内容前,先 truncate(0),再 seek(0)。否则后一次 JSON 更短,旧文件尾部残留的字符可能把整份记录弄坏。读取时只把"文件不存在"看作第一次使用;空文件、解析失败和读取异常,都不能直接当作空记录覆盖掉。

核心写入过程如下,和仓库中的 storage.js 保持一致:
javascript
async write(value){
try{
const directory=await provider.getDirectory();
const handle=await directory.getFileHandle(FILE,{create:true}),writer=await handle.createWritable();
await writer.truncate(0);await writer.seek(0);await writer.write(value);await writer.close();
if(await readFile(handle)!==value)throw Error('回读不一致');
}catch(e){console.warn('游戏记录保存失败',String(e));throw Error('保存未确认,请重新读取后继续');}
}
这里的回读是确认本次保存结果,不能把它说成任意两个窗口同时写入都绝对安全的数据库事务。写前比较能发现已发生的外部修改,但没有跨进程原子比较交换的保证。当前产品也没有多设备同步;它解决的是这台宿主上当前局和最近一局的保存问题。
页面读取失败时,会进入"先把记录读回来"的界面,并保留重新读取入口。保存失败则停止后续操作,先恢复到可信记录,再继续。错误提示要告诉用户下一步做什么,而不是一边写"保存失败",另一边还让人不停提交新词。

我分别检查了接上反馈、已揭晓但未提交、五轮结束三个恢复位置。它们不能都被恢复成普通接词页:接上反馈还在等待继续,揭晓后的这一轮还没有新增答案,结束的局则应该停在总结。恢复的是具体阶段,玩家才能判断刚刚的操作到底有没有完成。
10、这次语音测试的实际结果
这个项目有语音入口,但我当前这次模拟环境里,页内收音没有打通。日志出现了网络错误和 Failed to open browser media stream,页面给出"语音连接失败,请用按键接词"。这说明当前环境没能建立所需的收音链路,不能由此证明眼镜真机也会失败,也不能反过来说语音功能已经验收通过。

程序不在页面一打开就申请收音。只有玩家主动进入"说一条成语",或者宿主触发相应唤醒入口时,才尝试建立识别。启动等待十秒未就绪会结束等待;已经开始听之后,二十秒没有完成也会收束,不让"正在连接"无限停在页面上。
识别过程也分最终结果和临时结果。最终文本可以交给规则判定;临时文本只用于让用户知道正在听到什么,不能每变化一次就提交一条答案。用户主动单击结束时,可以在短暂等待后使用已经得到的内容;自然结束却没有最终结果时,不随便把半句话当答案。
退出页面、隐藏页面或重新运行时,旧识别会被终止,回调还要检查本次收音是否有效。否则有一种很难复现的问题:用户已经换局,上一局的识别结果晚到,突然把新局的状态改了。这里用递增的收音编号和页面活动状态,一起阻止迟到结果继续生效。
因此验证时要分清三件事:宿主能否理解意图并调页面,页内能否拿到识别文本,拿到文本后规则是否正确。当前完成的是规则与按键流程验证,语音失败的提示和退出处理也做了检查;真实语音链路和真机手势仍需要对应环境。这些结果放在一起,才是这一版实际能说明的范围。
11、演示效果:五条成语与四种来源
一局完整模拟从"一心一意"开始,实际接词链是:意气风发 → 发扬光大 → 大显身手 → 手不释卷 → 卷土重来。这里不是把五个正确词直接写进页面,而是经过候选、提示或揭晓,再逐条确认和继续。
第一条用候选接上,第二条逐层看到了完整答案,第三条再用候选。第四条先看一层释义,再打开候选完成,因此来源记为"借助提示";第五条通过候选完成。最后一条接上时先显示反馈,主动继续后才进入五轮结束,避免把第五条的释义一闪而过。
总结页实际显示"独立 0 · 候选 3 · 提示 1 · 揭晓 1"。没有独立回答,就如实显示零。按键演示本来就暴露了候选,给它硬凑一个独立成绩反而失去了记录来源的意义。

接下来进入逐词复盘。每条都保留成语、短释义和帮助来源,"上一条""下一条"到达边界时停在边界,不循环跳到另一头。看完可以回到本局,也可以回首页。下面的连续 GIF 展示了实际翻词过程。

"手不释卷"这一条尤其能检验记录是否正确:它最后是通过候选选中的,但之前已经用过释义提示,所以复盘显示的是"借助提示"。如果只在按下按钮那一刻判断来源,这条就会被错记成候选。把本轮帮助提前保存在状态里,最后展示时才有依据。

除了完成一局,我还检查了中途结束、确认页取消、另开一局留存旧局、重跑后继续,以及没有接上词时的空复盘。空复盘不应该显示一个假词或越界索引,开局词也不能冒充玩家完成的第一条。测试这类边角,是为了让普通流程里的每一步都有明确出口。
12、复盘翻页为什么冒出"请重试"
这次模拟操作确实找到了一个小问题:复盘翻页时想清空状态栏,调用了 notice(''),原实现却写成 text || '请重试'。空字符串被当成失败值,正常翻页下方反而冒出"请重试"。
它不影响成语链,却会让用户怀疑刚才有没有翻成功。修复时只让 null 和 undefined 使用兜底文案,空字符串就按"清空提示"处理。实际修复后的方法是:
javascript
notice(text){this.setData({statusText:text==null?'请重试':String(text)});},
修复后重新录了复盘动图,也把这条情况加进检查。页面上几乎每条短文案都会影响用户判断,不能因为核心规则正确,就忽略成功流程里突然出现的错误提示。
本地检查共 45 项,覆盖三组开局、词库连通、原字判定、重复词、提示层级、候选来源、过期选项、保存失败、外部记录变化、短文件覆盖、恢复阶段以及语音回调失效等。运行不需要额外 npm 依赖:
bash
node tests/verify.mjs
我没有只对本地目录跑一次就结束。平台导出的实际 AIX 中,模块已经经过处理,因此又提取实际模块执行同一组断言;公开仓库重新取得一份代码后,也再次运行这 45 项。三处通过,分别说明本地实现、平台包内逻辑和公开源码在这些检查范围内保持一致。
测试也有边界。逻辑检查不能证明设备上的收音可用,模拟器按钮成功不能证明真实镜腿事件完全相同,AIX 内容正确也不能保证商店审核通过。每项结果对应一个具体环节,把它们拼起来,才比一句"没有问题"更有参考价值。
想自己复现,可以先选"一心一意"开局,进入按键候选,故意选错一条,确认计数仍是零;随后接上正确项,停在反馈页点 Studio 的重新运行,检查这一条和反馈阶段都在。接着进入下一轮,把提示看到完整词,再次重新运行,确认计数没有因揭晓而增加。最后走完五轮,对照总结和逐词来源。这条路线能把"失败不推进、成功先保存、提示不自动提交、恢复不改记录"串在一起,比只点一次开始更有用。
13、源码、平台包与提交记录
打包时只收应用需要的文件,不把开发过程里的截图、文章、录制帧、测试输出或仓库元数据一股脑带进去。这个工程的应用文件和测试脚本共 15 个公开文件,具体清单放在仓库;平台运行包由平台生成,并再次核对页面和参数。

这里容易把三个版本混在一起:源码工程的原生版本是 1.0.0,平台保存过的调试包是 1.0.2,提交审核后平台显示的是 1.0.3。它们属于不同环节,不能只凭页面上的数字变化就判断上传了另一份代码。我核对的是包名、页面路径、参数定义和实际导出包内容,再分别记录各自的版本。
提审使用了独立图标、四张实际预览图和一段约三十秒的实际演示视频。视频约 315 KB,已在平台预览并确认可播放,素材上传也得到平台接受。这里不猜平台未明确显示的大小上限,只记录本次文件和上传结果;后续重新提交仍应以当时页面要求为准。
截至本文这次核对,平台显示"接着说"v1.0.3 于 2026-10-23 20:08 提交,状态是"审核中"。这张图记录的是提交结果,后续是否通过,以平台审核反馈为准。

14、总结:这一局借过哪些帮助
这五条里最容易漏记的,是"手不释卷":最后从候选选中,前面却已经看过释义。复盘仍显示"借助提示",这条记录对上了,提示、候选和提交之间的关系才算说清。
后续想扩词库,就沿着现有首尾链加一条完整分支,并补短释义和用例。真实收音还要换到可用环境里复验。一局先保持五轮,玩完能回头找到每条词,比先把词数做大更有用。
资料与素材
技术接口:AIUI OPFS接口。