在眼镜上“解梦看黄历“——梦解黄历智能体的开发手记
做一个 AR 眼镜上的民俗小工具,这是最初的念头。
Rokid AR 眼镜的交互方式和手机完全不同——没有触摸屏,用户靠语音和侧边按键操作,显示面积只有 480×352 像素。这个条件天然适合"说一句话就得到答案"的场景,不适合复杂的表单或深度浏览。
结合民俗文化里两个高频需求——周公解梦和黄历查询,我做了「梦解黄历」。用户唤醒后说一句"我梦见蛇"或"明天黄历",智能体在卡片上展示解读结果并同步语音播报,一次交互同时拿到梦境参考和当日宜忌。目标用户是对民俗文化感兴趣的普通用户,使用场景往往是清晨醒来想起昨晚的梦,或者出门前想看看今天的宜忌。

一、场景与用户意图
用户的典型使用路径是这样的:
-
清晨醒来,说"Hi Rokid,打开梦解黄历",智能体播报开场白,同时展示今日黄历卡片
-
接着说"我梦见蛇",系统匹配民俗解读规则库给出参考释义,并在卡片下方附带今日宜忌
-
追问"好不好",系统根据梦境内容判断倾向——偏吉利、偏警示或吉凶参半,措辞回避"大吉""大凶"等绝对断语
-
或者直接说"明天黄历"“下周一宜忌”“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.真机验证
真机上三条核心链路仍待实跑验证:
-
语音唤醒 → 开场白播报
-
语音识别 → 解梦 → 追问吉凶(先说"我梦见蛇"再说"好不好"的连续对话流)
-
语音识别 → 日期提取 → 黄历展示(“明天黄历”“下周一宜忌”"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 兜底的质量和稳定性需要持续打磨
-
真机验证:三条核心链路需要设备实跑,特别是语音识别的准确率和追问场景的上下文衔接。
更多推荐



所有评论(0)