做一个 AR 眼镜上的民俗小工具,这是最初的念头。

Rokid AR 眼镜的交互方式和手机完全不同——没有触摸屏,用户靠语音和侧边按键操作,显示面积只有 480×352 像素。这个条件天然适合"说一句话就得到答案"的场景,不适合复杂的表单或深度浏览。

结合民俗文化里两个高频需求——周公解梦和黄历查询,我做了「梦解黄历」。用户唤醒后说一句"我梦见蛇"或"明天黄历",智能体在卡片上展示解读结果并同步语音播报,一次交互同时拿到梦境参考和当日宜忌。目标用户是对民俗文化感兴趣的普通用户,使用场景往往是清晨醒来想起昨晚的梦,或者出门前想看看今天的宜忌。

一、场景与用户意图

用户的典型使用路径是这样的:

  1. 清晨醒来,说"Hi Rokid,打开梦解黄历",智能体播报开场白,同时展示今日黄历卡片

  2. 接着说"我梦见蛇",系统匹配民俗解读规则库给出参考释义,并在卡片下方附带今日宜忌

  3. 追问"好不好",系统根据梦境内容判断倾向——偏吉利、偏警示或吉凶参半,措辞回避"大吉""大凶"等绝对断语

  4. 或者直接说"明天黄历"“下周一宜忌”“3月15号好不好”,系统识别日期并展示对应黄历

智能体的意图理解完全靠本地规则——intent.js 里用关键词匹配做意图分类(解梦、黄历查询、追问、退出),parser.js 做梦境关键词提取,guard.js 拦截算命、择日等超出范围请求。规则匹配不到的梦境走 LLM 兜底,但兜底回复同样经过 guard.sanitizeModelReply 清洗——把"预示着"“必将"等断言性表述替换为"常被联系到”,并强制附加免责声明。

二、方案设计

1.智能体定义

应用入口是 app.js,注册了全局的 onLaunch、playGreeting、resetSession 等方法。app.json 只声明了一个页面 pages/index/index 和 RECORD_AUDIO 权限——语音识别需要录音权限,没有声明的话真机上会直接失败。

2.页面与卡片设计

整个应用只有一个页面 index.ink,采用单文件模式(<script def> + <script setup> + <page> + <style> 四块平级)。页面结构从上到下分为:

  • 标题栏:「梦解黄历」

  • 状态条:一个圆点指示当前状态(idle/listening/error),右侧文字提示当前操作

  • 卡片区:scroll-view 可滚动,展示解梦或黄历的 Markdown 解析结果

  • 分割线(1px view 元素,不用单侧 border)

  • 底部提示:操作引导文字

视觉上遵循单色绿设计规范——主色 #40ff5e,层次用 rgba(64,255,94,0.72/0.48/0.24/0.12) 搭配,背景纯黑。选中态和状态用透明度差异区分,不只靠颜色。

3.状态流转

页面维护一个 voiceState 字段,取值 idle → listening → recognizing → idle(正常路径)或 error(异常路径):

  • onVoiceWakeup 事件触发 startListening,创建 SpeechRecognition 实例,设置 interimResults = false(只处理 final 结果,设 true 会报 NotSupportedError)

  • rec.onresult 拿到识别文本后调 handleUtterance,进入路由分发

  • rec.onerror 保留 err.message(不只是 error code,因为语音识别的 network 错误码覆盖面太广,真因在 message 里)

  • 12 秒超时兜底:没听到声音就自动 stop,回到 idle 态

  • 双击(Backspace 键)在 onKeyUp 里调 event.preventDefault() 取消正在进行的听写

handleUtterance 是核心路由——调用 router.handleInput(text, ctx),拿到 { mode, card, voice } 后用 parseCardLines 把 Markdown 拆成行数组渲染到卡片,同时用 wx.speech.playTTS 播报语音。

4.能力调用关系

用户语音 → SpeechRecognition → handleUtterance → router.handleInput
  → guard.isForbidden? → 拦截
  → intent.isCalendarOnly? → extractDate → huangli.getDate → 展示黄历
  → intent.isFollowUp + memory.currentDream? → zhougong.followUp → 追问回答
  → intent.isDream / parser.hasDreamCue? → zhougong.match → 规则匹配
    → 匹配成功 → 展示解读
    → 匹配失败 → llmClient.interpretDreamFallback → LLM 兜底
      → 兜底 → guide 引导
  → utils.buildCard / buildVoice → 卡片 + TTS

memory.js 维护会话状态——currentDream 存最近一次解梦的文本和关键词,追问时读取这个上下文判断吉凶倾向。

三、AIUI 实现

1.项目结构

app.json          应用配置(页面注册、权限声明)
app.js            应用入口(开场白 TTS、会话管理)
pages/index/
  index.ink       单文件页面(语音识别、卡片渲染、按键交互)
lib/
  router.js       意图路由(分发到解梦/黄历/追问/退出)
  intent.js       意图识别 + 日期提取(纯整数算术)
  parser.js       梦境关键词提取
  guard.js        内容护栏(禁词过滤、免责声明)
  memory.js       会话状态
  utils.js        卡片/语音构建
  llmClient.js    LLM 兜底调用
data/
  zhougong.js     周公解梦规则库(29 条 + 吉凶倾向表)
  huangli.js      黄历数据(农历近似算法 + 宜忌池)

2.Ink 页面关键实现

页面用 onKeyDown 接收按键事件——Rokid 眼镜运行时把硬件按键转为标准键事件下发,不做点击转换。Enter 唤醒听写,ArrowUp/ArrowDown 滚动卡片(scroll-view 的 scroll-top 通过 setData 控制,加了 300ms 去重窗防抖),双击(Backspace)在 onKeyUp 里 preventDefault 取消听写。

语音识别用的是全局构造器 new SpeechRecognition()——不是 wx 模块的方法,也不是 import 进来的。interimResults 保持默认 false,只处理 onresult 的 final 结果。

3.内容护栏

guard.js 有两层防护:

  • 入口拦截:FORBIDDEN_INTENT 列表匹配"算命"“八字”“适合结婚”"适合签约"等择日/算命类请求,直接返回引导理性看待的回复

  • 输出清洗:sanitizeModelReply 对 LLM 兜底回复做处理——命中"注定"“大凶”“必破财"等禁词时替换为安全文案,把"预示着"改写为"常被联系到”,强制附加免责声明

吉凶追问的回答也遵守这个原则——zhougong.followUp 的吉凶分支用"偏吉利"“偏警示”“吉凶参半"代替"大吉”“大凶”,倾向性表(AUSPICIOUS_CUES / INAUSPICIOUS_CUES)只给方向不给定论。

四、交互与体验

1.主要界面

首屏进入时展示欢迎卡片——标题「梦解黄历」,正文是今日黄历(公历、农历、宜忌各 5 条),底部提示「唤醒词:Hi Rokid,打开梦解黄历」。同时 TTS 播报开场白,告诉用户可以做什么。

用户说"我梦见蛇"后,卡片切换为梦境解读——先展示匹配到的民俗释义(几句话,接地气的表述),下方附带今日宜忌。语音播报会读解读的第一句加今日宜忌摘要,一条语音同时覆盖两个信息。

追问"好不好"时,卡片切换为吉凶倾向回答,措辞克制——“从民俗说法来看,这个梦偏吉利"而不是"大吉”。

2.操作路径与反馈

  • 唤醒 → 自动进入听写态,状态点变亮,文字提示"正在听…"

  • 说话完毕 → 自动结束识别(continuous = false),文字提示"识别中…"

  • 结果返回 → 卡片更新,语音播报,状态回到 idle

  • 12 秒没声音 → 自动超时回到 idle,提示"没有听到,请唤醒后再说"

  • 双击 → 取消正在进行的听写,回到 idle

  • 上下滑动(ArrowUp/ArrowDown)→ 滚动卡片内容

  • 说"退出"/“再见” → 播报"梦解黄历已关闭,期待下次使用",重置会话

五、调试与验证

1.AIUI Studio 预览

在 AIUI Studio 里通过 onSimulateInput 注入文本模拟语音输入,验证了以下路径:

  • “我梦见蛇” → 匹配规则库 → 展示解读 + 今日宜忌

  • “好不好” → 走追问吉凶分支 → 偏警示倾向回答

  • “明天黄历” → extractDate 返回明天日期 → 展示明天黄历

  • “下周一宜忌” → 周几匹配 → 已过本周自动取下周

  • “3月15号黄历” → 绝对日期匹配 → 已过取明年

  • “明天是什么日子” → isCalendarOnly 匹配 → 走黄历展示路径

  • “哪天适合结婚” → guard 拦截 → 引导理性看待

静态校验 0 error。warning 主要是校验快照未包含全部工作区文件导致的 import 未找到提示,非实际问题。

2.日期 2060 年 bug 的排查

这是开发过程中最有代表性的一次调试。

用户反馈"明天是什么日子"时页面日期显示为 2060 年。最初以为是 extractDate 逻辑写错,检查后发现逻辑正确——问题出在 QuickJS 的 Date 多参数构造器在真机上返回了错误初始化的日期对象。getFullYear() 返回 2060 而不是 NaN,说明对象本身不是 Invalid Date,而是构造器内部年月日计算出了偏差。

第一轮修复只改了 extractDate 的偏移方式(new Date(dateObject) → new Date(timestamp)),但 todayTs 本身仍依赖多参数构造器,问题没消除。第二轮把三个文件联动改造——extractDate 返回纯整数、getDate 接收纯整数、router 传纯整数,彻底移除了整条链路上对 Date 构造器做年月日运算的依赖。第三轮发现绝对日期分支还残留了一个 new Date(y, m, d).getDay(),也改为纯整数 weekday 计算。

3.真机验证

真机上三条核心链路仍待实跑验证:

  1. 语音唤醒 → 开场白播报

  2. 语音识别 → 解梦 → 追问吉凶(先说"我梦见蛇"再说"好不好"的连续对话流)

  3. 语音识别 → 日期提取 → 黄历展示(“明天黄历”“下周一宜忌”"3月15号好不好"等输入)

这些链路涉及语音识别的端到端行为,只能在设备上确认。

六、成果与复盘

1.可运行成果

  • 周公解梦:29 条民俗解读规则 + LLM 兜底,解读后自动附带今日宜忌

  • 梦境追问:支持吉凶倾向回答(偏吉利/偏警示/吉凶参半)和补充解读(“换个说法”)

  • 黄历查询:支持今天、相对日期(明天/后天/大后天)、周几(下周一/这周五)、绝对日期(3月15号)和自然问法(明天是什么日子)

  • 内容护栏:禁词过滤 + 择日拦截 + 免责声明,所有输出回避绝对断语

  • 单文件页面:语音识别、卡片渲染、按键交互、滚动浏览,单屏 480×352 完整呈现

2.遇到的问题与解决

  • 日期显示 2060 年:QuickJS 的 Date 多参数构造器在真机上有实现差异,改为纯整数算术彻底绕过

  • 追问吉凶答非所问:router.js 调用 followUp 时只传梦境对象没传追问原文,导致吉凶类追问返回的是补充解读。修复为调用前合并 lastFollowUpText 字段

  • 应用改名:原名"梦历阁"古风偏重,改为"梦解黄历"——功能一目了然,“解梦”+"黄历"两个关键词都在名字里,全量替换 6 个文件

3.后续迭代计划

  • 农历精度:当前用近似算法 getLunarLike(公历月日偏移 8 个月做粗略映射),非精确农历转换,后续可引入准确的农历库

  • 宜忌丰富度:宜忌条目池目前只有 4 组 × 5 条,按日期种子取模选择,条目偏少,后续可扩充 YI_POOL / JI_POOL

  • 规则库扩展:解梦规则 29 条覆盖了常见梦境,但用户梦境表达千变万化,LLM 兜底的质量和稳定性需要持续打磨

  • 真机验证:三条核心链路需要设备实跑,特别是语音识别的准确率和追问场景的上下文衔接。

Logo

更多推荐