打造专属“贾维斯”:基于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;
}

请添加图片描述
在这里插入图片描述
在这里插入图片描述

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐