打造专属“贾维斯”:基于Rokid glasses + openclaw的家庭AI助手
打造专属“贾维斯”:基于Rokid glasses + openclaw的家庭AI助手
本文应用基于Rokid灵珠智能体/CXR SDK开发,开发指南https://forum.rokid.com/index
一、开发背景
1.《钢铁侠》在我心中埋下了家庭AI助手的种子
记得以前看《钢铁侠》的时候,史塔克随身唤醒贾维斯之后,能够让贾维斯帮他出谋划策、帮他处理家中的电器,当时感觉还遥不可及。眨眼间,AI的发展、Rokid glasses的出现,让我拥有一个贾维斯的愿望愈发强烈。
2. Rokid glasses AI的使用体验
从F码开始使用到现在,愈发的感觉到乐奇的使用场景非常有限。例如创建的待办、日程,当我想在手机上查看内容的时候,我只能打开Rokid AI -> 主页 -> 便签 来查看。有的时候在户外,哪怕是亮度最高,用眼镜也根本看不见,但是打开APP又很繁琐,让我根本不想使用Rokid的待办功能。
3. 家里的家电设备有很多,我能不能不用手机张嘴就能控制呢?
我家中的很多设备,各个品牌的都有:海尔的冰箱、卡萨帝的空调、小米的电视/台灯、HP的打印机等,真的是五花八门,手机上也下了一整页的品牌APP。我就想,能不能一个入口,也不用掏手机,就像《钢铁侠》中的贾维斯一样,张嘴就能控制呢?我的目光就转向了Rokid glasses,嗯?有了!
二、开发过程
1. 打通各品牌智能家居的思路
说干就干,首先想一下如何才能实现通过Rokid glasses 实现对智能家居的控制。要想实现对家电的控制,首先要通过各品牌提供的API来控制——哎,有了,HomeAssistant!它可以把不同品牌的家电集成到一起,正好我也有NAS,嘿,天时地利人和!
下一步是如何把Rokid glasses和HomeAssistant打通呢?这时候OpenClaw横空出世,刹那间,天翼大佬的灵珠平台对接OpenClaw的教程也出来了,哎嘿,这一下子思路明确了啊(激动地搓手手)!
2. 待办如何更方便的使用和查看呢?
我自己最常用的笔记以及待办工具就是Trilium,我就想,如果能把Rokid下达的任务直接推送到Trilium,我就不用再打开电脑建任务了。但是手机端又没有Trilium的APP,看起来还是不方便。研究了一下发现,哎嘿,Trilium有ETAPI,一股邪恶的笑声在我心中响起,桀桀桀桀桀桀桀桀桀桀桀桀桀桀桀桀桀桀,有思路了!
3. 欲知后事如何,且看下回分解
三、技术方案
这是总体的流程图,我们主要讨论的是流程思路,所以不对APP开发等细节进行详细描述。
四、代码实现
大部分内容都根据官方的API文档及OpenClaw的配置进行,所以我的流程图看明白了基本上自己就可以复现。代码主要说一下OpenClaw的Skill和APP开发的部分代码。
手机端:获取今日待办事项列表(包含待办ID)
/**
* 获取今日待办事项列表(包含待办ID)
* 功能:调用Trilium的ETAPI接口,根据指定日期(今日)搜索待办事项,
* 解析返回的JSON数据并封装成TodoInfo对象列表返回
* @return 今日待办事项的TodoInfo列表,若无数据则返回空列表
*/
public static List<TodoInfo> fetchTodayTodosWithId() {
// 初始化待办事项列表,用于存储最终解析后的待办数据
final List<TodoInfo> todos = new ArrayList<>();
// 异常捕获块:捕获接口请求、JSON解析、IO操作等过程中可能出现的所有异常
try {
// 1. 构建今日日期字符串,格式为"yyyy-MM-dd"(例如2026-02-26)
// Locale.getDefault():使用系统默认的区域设置,保证日期格式适配当前系统
String today = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date());
// 2. 构建Trilium搜索查询语句
// #startDate是Trilium中待办事项的日期属性,= 表示精确匹配,单引号包裹日期字符串是Trilium的搜索语法要求
String searchQuery = "#startDate = '" + today + "'";
// 对搜索语句进行URL编码(UTF-8),避免特殊字符(如空格、=、')导致URL解析错误
String encodedQuery = java.net.URLEncoder.encode(searchQuery, "UTF-8");
// 拼接完整的搜索URL:
// TRILIUM_URL:Trilium服务的基础地址(全局常量)
// /etapi/notes:Trilium ETAPI的笔记搜索接口路径
// search=encodedQuery:携带编码后的搜索条件
// ancestorNoteId=123123123123:限定搜索范围为指定父笔记下的子笔记(123123123123是父笔记ID)
String searchUrl = TRILIUM_URL + "/etapi/notes?search=" + encodedQuery + "&ancestorNoteId=123123123123";
// 打印调试日志:输出最终拼接的搜索URL,便于排查URL拼接错误
Log.d(TAG, "搜索URL: " + searchUrl);
// 3. 创建HTTP连接并配置请求参数
// 将字符串URL转换为URL对象
URL url = new URL(searchUrl);
// 打开HTTP连接并强制转换为HttpURLConnection(支持设置请求方法、请求头、超时等)
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
// 设置请求方法为GET(Trilium的搜索接口要求GET请求)
connection.setRequestMethod("GET");
// 设置Authorization请求头:TOKEN是全局常量,用于Trilium接口的身份认证
connection.setRequestProperty("Authorization", TOKEN);
// 设置Content-Type请求头:告知服务端请求体为JSON格式(虽然GET无请求体,但规范配置可避免兼容问题)
connection.setRequestProperty("Content-Type", "application/json");
// 设置连接超时时间:10秒(10000毫秒),超过该时间未建立连接则抛出超时异常
connection.setConnectTimeout(10000);
// 设置读取超时时间:10秒(10000毫秒),超过该时间未读取到响应数据则抛出超时异常
connection.setReadTimeout(10000);
// 获取接口响应码(如200表示成功,401表示未授权,500表示服务端错误等)
int responseCode = connection.getResponseCode();
// 打印调试日志:输出响应码,便于排查接口请求失败原因
Log.d(TAG, "响应码: " + responseCode);
// 4. 处理成功的响应(响应码为200)
if (responseCode == HttpURLConnection.HTTP_OK) {
// 创建BufferedReader读取响应流:InputStreamReader将字节流转换为字符流,BufferedReader提高读取效率
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
// 初始化StringBuilder:用于拼接响应的每行字符,最终形成完整的JSON字符串
StringBuilder response = new StringBuilder();
// 临时变量:存储每次读取的一行字符
String line;
// 循环读取响应流:直到读取到末尾(line为null)
while ((line = reader.readLine()) != null) {
// 将每行字符拼接到StringBuilder中
response.append(line);
}
// 关闭Reader:释放IO资源,避免内存泄漏
reader.close();
// 将StringBuilder转换为字符串:得到完整的JSON响应内容
String responseStr = response.toString();
// 打印调试日志:输出前500个字符(避免日志过长),便于查看响应数据格式
Log.d(TAG, "响应内容: " + responseStr.substring(0, Math.min(500, responseStr.length())));
// 5. 解析JSON响应数据
// 将JSON字符串转换为JSONObject:根节点是对象
JSONObject jsonResponse = new JSONObject(responseStr);
// 从根对象中获取"results"数组:该数组存储了搜索到的所有待办笔记
JSONArray results = jsonResponse.getJSONArray("results");
// 打印调试日志:输出找到的待办事项数量
Log.d(TAG, "找到 " + results.length() + " 条结果");
// 遍历results数组:逐个解析每个待办笔记
for (int i = 0; i < results.length(); i++) {
// 获取数组中的第i个笔记对象
JSONObject note = results.getJSONObject(i);
// 从笔记对象中获取"title"字段:待办事项的标题
String title = note.getString("title");
// 从笔记对象中获取"noteId"字段:待办事项的唯一ID
String noteId = note.getString("noteId");
// 6. 解析待办事项的startTime属性(存储在attributes数组中)
// 初始化time变量:默认空字符串,若未找到startTime则返回空
String time = "";
// 从笔记对象中获取"attributes"数组(optJSONArray避免数组不存在时抛出异常)
JSONArray attrs = note.optJSONArray("attributes");
// 若attributes数组不为空,则遍历查找startTime属性
if (attrs != null) {
for (int j = 0; j < attrs.length(); j++) {
// 获取数组中的第j个属性对象
JSONObject attr = attrs.getJSONObject(j);
// 判断属性名称是否为"startTime"(optString避免字段不存在时抛出异常)
if ("startTime".equals(attr.optString("name"))) {
// 获取startTime属性的值:待办事项的具体时间(如09:00)
time = attr.optString("value", "");
// 找到后跳出内层循环:无需继续遍历其他属性
break;
}
}
}
// 7. 将解析后的待办数据封装为TodoInfoItem(TodoInfo的实现类)并添加到列表中
todos.add(new TodoInfoItem(title, noteId, time));
// 打印调试日志:输出单个待办事项的详细信息,便于验证解析结果
Log.d(TAG, "待办: " + title + " (时间: " + time + ", ID: " + noteId + ")");
}
} else {
// 处理非200的响应码:打印错误日志,提示API请求失败
Log.e(TAG, "API请求失败,响应码: " + responseCode);
}
// 关闭HTTP连接:释放网络连接资源,避免资源占用
connection.disconnect();
} catch (Exception e) {
// 捕获所有异常:包括IO异常、JSON解析异常、URL编码异常等
// 打印错误日志:输出异常信息,便于定位问题
Log.e(TAG, "获取待办事项时发生错误: " + e.getMessage());
// 打印异常堆栈:输出完整的异常调用链,便于排查根因
e.printStackTrace();
}
// 返回待办事项列表:无论是否有数据,都返回列表(空列表或有数据的列表)
return todos;
}



更多推荐
所有评论(0)