本帖最后由 hhh. 于 2026-5-20 21:55 编辑
前言:
实现UI界面,需要使用和了解大量的卡片信息,构建符合自己审美和肩负易操作,好实现的UI界面,是很不容易的一件事。
而“智能家居”的最终目标,我觉得不是UI如何好看,设备如何丰富,语音如何智能,操作如何简单。而是完全基于“自动化而”实现的可持续,可扩展,易维护的“个人管家”。
那么我们先迈出第一步,搭建“个人管家”的基础框架:
思路:数据获取→数据处理→回复结果。
- 数据获取,顾名思义,就是数据来源,无平台限制,任何通信平台数据来源,支持接入HA的都可以。(只支持字符串)
- 数据处理,根据指令,执行指令中需要执行的自动化,无任何限制,目的就是数据处理。细分一下:{指令判断、分类},执行,输出。
- 回复结果,根据不同平台的回复规则,执行不同平台的回复代码。
数据获取:
按照微信接入HA的一种方法帖子中的方法,我们通过”cn_im_hub“ + ”Node-RED Conversation Agent”集成实现了微信接入HA,那么下面我们探讨,如何处理获取后的数据。
流程构造:events:all节点(监听nodered_request_event) → function节点(处理) → link out节点(输出)。
→ function节点(存储)
function节点(处理)示例: 将来源数据打上特定标签,用户后续流程判断。
// 提取事件数据
let eventData = msg.payload.event;
if (!eventData) return null;
let requestId = eventData.request_id || "";
let Label = "cn_im_hub";
// 整理输出
msg = {
payload: eventData.text, // 消息
request_id: requestId, // 通信ID
Label: Label // 判定标签
};
return msg;
function节点(存储)示例: 将,用户ID账户ID,持久化存储。
let req = msg.request_id || "";
let parts = req.split(":");
// 来源
let source = parts[0] || "";
// 仅处理 wechat 来源
if (source === "wechat") {
// wechat:<prefix>@im.bot:<userId>@im.wechat
let fullBotId = parts[1] || ""; // 账号
let prefix = fullBotId.split("@")[0] || ""; // 账号识别码
let userId = parts[2] || ""; // 用户ID
// 生成 key 名称(固定,不编号)
let botKey = `${prefix}_Bot_id`;
let userKey = `${prefix}_User_id`;
// 读取当前存储
let existingBot = flow.get(botKey, "file");
let existingUser = flow.get(userKey, "file");
// --- Bot_id:只要 prefix 不变,就永远覆盖,不新增 ---
if (existingBot !== fullBotId) {
flow.set(botKey, fullBotId, "file");
}
// --- User_id:同理,只要 prefix 不变,就永远覆盖,不新增 ---
if (existingUser !== userId) {
flow.set(userKey, userId, "file");
}
// 状态显示
node.status({
fill: "green",
shape: "dot",
text: `用户识别码: ${prefix}`
});
}
return msg;
企业微信接入同理。
数据处理:
指令判断:link in节点(上联数据获取) → function节点(判断) → function节点(数据输出)
不等于任何规则的代码示例:
function节点(判断): 判断:如果其他模式(vx_mode)未激活,来源消息与规则数组不匹配,就输出,否则阻止。
// 获取输入内容并去除空格
let payload = String(msg.payload || "").trim();
// 读取持久化状态,如果已激活则阻断
let modeActive = flow.get("wx_mode", "file");
if (modeActive === true) {
// 已激活模式,不输出
return null;
}
// 安全获取持久化全局数组
const andOrArray = flow.get("AND/ORarray", "file") || {};
const excludeKeywords = Object.values(andOrArray).flat();
// 保留全局关键字阻断
if (excludeKeywords.some(k => payload.includes(k))) {
return null;
}
// 输出 payload
msg.payload = payload;
return msg;
大家应该注意到其中,有两段基于持久化的判定代码,目的就是判断流程走向。
- let modeActive = flow.get("wx_mode", "file"); 判断是否激活其他模式,如:大模型。(已激活 小爱同学 大模型,基于此)
- const andOrArray = flow.get("AND/ORarray", "file") || {}; 根据数组中的默认规则匹配流程走向。
而此阶段就是流程的精髓,也就是判断流程走向的重中之重,其中是否激活其他模式的来源就是,大模型流程中的 function节点(判断)。
激活其他模式 function节点(判断)示例: 判断:来源消息包含:小爱同学等,打开其他模式。
let payload = String(msg.payload || "").trim();
// 激活关键字及对应 flow key
const keywords = {
"通义千问": "wx_ali_mode",
"小爱同学": "wx_xiaoai_mode",
"aihub": "wx_aihub_mode"
};
for (let key in keywords) {
if (payload.includes(key)) {
// 持久化保存对应的激活状态
flow.set(keywords[key], true, "file");
flow.set("wx_mode", true, "file");
msg.payload = `已激活 ${key} 模式`;
return msg;
}
}
// 其他情况阻断,不输出
return null;
而规则数组的目的就是减少代码量,重复使用。
代码示例: 规则持久化存储
flow.set("AND/ORarray", {
Ai: [
"小爱同学",
"aihub",
"r1",
"通义千问"
],
Specific: [
"你能做什么",
"测试",
"帮助"
],
Device: [
"阳台窗帘纱",
"阳台窗帘布",
"主卧窗帘纱",
"主卧窗帘布",
"次卧窗帘"
],
Custom: [
"今日天气",
"现在天气",
"日程",
"农历",
"真太阳时",
"室内环境监控",
"门窗状态",
"室内光照情况",
"电池电量预警",
"电池电量总览",
"Debian13备份状态"
],
Plan: [
"休息日",
"工作日",
"外出"
],
Special: [
"重置阿里对话",
"数组初始化",
"退下吧"
]
}, "file"); // 指定持久化存储类型
node.status({fill:"green", shape:"dot", text:"AND/ORarray 持久化完成"});
let message = "数组初始化完成";
// 输出
msg.message = message;
return msg;
而借此规则,我们可以衍生出基于此规则的,流程走向,这就是将指令“分类”。
我们将流程再延申:
link in节点(上联数据获取) → function节点(判断) → link call(格式化日期) → function节点(分类) → 其他节点(数据获取) → function节点(数据输出)
function节点(判断)示例: 判断:如果其他模式(vx_mode)未激活,来源消息与规则数组匹配,就输出,否则阻止。
let payload = String(msg.payload || "").trim();
// 读取持久化状态,如果已激活则阻断
let modeActive = flow.get("wx_mode", "file");
if (modeActive === true) {
// 已激活模式,不输出
return null;
}
// 安全获取 Custom 数组(持久化存储)
const andOrArray = flow.get("AND/ORarray", "file") || {};
const match = andOrArray.Custom || [];
// 完全匹配时输出时间戳
if (match.includes(payload)) {
msg.payload = Date.now(); // 毫秒级时间戳
msg.tag = payload; // 标签直接使用汉字
return msg;
}
// 不匹配时阻断消息
return null;
function节点(分类)例如农历获取流程示例: 判断:来源消息与特定标签匹配,就输出,否则阻止。
let tag = String(msg.tag).trim();
// 指定要匹配的精确值
const match = "农历";
if (match.includes(tag)) {
msg.payload = Date.now(); // 输出时间戳
return msg;
}
// 不匹配时阻断消息
return null;
其他节点(数据获取)农历数据示例: 数据获取后格式化输出。
// 基础数据
const Now = new Date(msg.payload);
const Year = Now.getFullYear();
const Month = Now.getMonth() + 1;
const Day = Now.getDate();
// 农历
const LunarDate = Lunar.Lunar.fromDate(Now);
// 节气(含倒计时)
const CurrentJieQi = LunarDate.getCurrentJieQi();
const NextJieQi = LunarDate.getNextJieQi();
let JieQiInfo = "节气数据不可用";
if (CurrentJieQi) {
// 今天正好是节气
JieQiInfo = `今天是${CurrentJieQi._p.name}`;
}
else if (NextJieQi) {
const nextSolar = NextJieQi.getSolar();
// 用年月日构造 Date,避免 toYmd() 解析失败
const nextDate = new Date(
nextSolar.getYear(),
nextSolar.getMonth() - 1,
nextSolar.getDay()
);
const currentDate = new Date(
Now.getFullYear(),
Now.getMonth(),
Now.getDate()
);
const diffMs = nextDate.getTime() - currentDate.getTime();
const diffDays = Math.ceil(diffMs / 86400000);
JieQiInfo = `${diffDays}天后是${NextJieQi.getName()}`;
}
// 输出
const lunar = {
"农历": LunarDate.getMonthInChinese() + "月" + LunarDate.getDayInChinese(),
"干支": {
"月": LunarDate.getMonthInGanZhiExact() + "月",
"日": LunarDate.getDayInGanZhiExact() + "日"
},
"生肖": LunarDate.getDayShengXiao(),
"日禄": LunarDate.getDayLu(),
"节气差": JieQiInfo,
"物候": LunarDate.getHou() + " " + LunarDate.getWuHou(),
"东方星宿": LunarDate.getGong() + "方" + LunarDate.getXiu() + LunarDate.getZheng() + LunarDate.getAnimal() + "-" + LunarDate.getXiuLuck(),
"十二天神": LunarDate.getDayTianShen() + "(" + LunarDate.getDayTianShenType() + ") " + LunarDate.getDayTianShenLuck(),
"建除日": LunarDate.getZhiXing() + "执位",
"冲煞": LunarDate.getDayShengXiao() + "日 冲" + LunarDate.getDayChongDesc() + " 煞" + LunarDate.getDaySha(),
"彭祖干": LunarDate.getPengZuGan(),
"彭祖支": LunarDate.getPengZuZhi(),
"宜": LunarDate.getDayYi().join(". "),
"忌": LunarDate.getDayJi().join(". "),
"胎神": LunarDate.getDayPositionTai(),
"吉神": LunarDate.getDayJiShen().join(". "),
"凶煞": LunarDate.getDayXiongSha().join(". ")
};
// 推送文本
const title = msg.titles || "";
const dateStr = msg.today.dateweekStr;
const message =
`${title}
📅 ${dateStr}
🌙 农历:${lunar.农历}
🪐 干支:${lunar.干支.月}${lunar.干支.日}
🐍 生肖:${lunar.生肖}
📌 日禄:${lunar.日禄}
🌞 节气:${lunar.节气差}
🍃 物候:${lunar.物候}
🌌 星宿:${lunar.东方星宿}
⚡ 吉日:${lunar.十二天神}
🟢 五行:${lunar.建除日}
💥 冲煞:${lunar.冲煞}
🔢 彭祖干:${lunar.彭祖干}
🔢 彭祖支:${lunar.彭祖支}
✅ 宜:${lunar.宜}
❌ 忌:${lunar.忌}
👶 胎神:${lunar.胎神}
🎯 吉神:${lunar.吉神}
☠️ 凶煞:${lunar.凶煞}`;
// 输出
msg.message = message;
return msg;
这个延申的流程,已经具备,“执行”和“输出”的能力,当然有人会说,这个流程完全可以在一个function节点中实现,但是别忘了:我们实现分类判断的目的,而且基于分类判断,我们的流程将不在只为一个流程服务,而是基于标签规则中的所有需要此类流程信息的高级流程而服务。(后续会探讨)
回复结果:
流程构造:function节点(前置判断,数据干净) → function节点(构造消息) → action节点(消息推送)
→ function节点(构造消息) → fire evevt节点(消息回复)
function节点(前置判断,数据干净)示例: 前置判断和数据干净
// 判断 cn_im_hub 消息
if (msg.Label !== "cn_im_hub" && msg.Label !== "all") { // 前置判断,获取数据流程中的 判定标签
return null;
}
// 获取字段
let message = msg.message || "";
let request_id = msg.request_id || "";
let channel = msg.channel || "";
let user_id = msg.user_id || "";
let bot_id = msg.bot_id || "";
// 1️⃣ 如果 channel 有数据 → 输出第 1 口
if (channel) {
return [
{
message: message,
channel: channel,
user_id: user_id,
bot_id: bot_id
},
null
];
}
// 2️⃣ 如果 request_id 有数据 → 输出第 2 口
if (request_id) {
return [
null,
{
message: message,
request_id: request_id
}
];
}
// 3️⃣ 两者都没有 → 不输出
return null;
主动推送 function节点(构造消息) 示例:
let channel = msg.channel;
let message = msg.message;
let userId = msg.user_id;
let botId = msg.bot_id;
msg.payload = {
"channel": channel, // 平台
"message": message, // 消息
"target": userId, // 用户ID
"wechat_account_id": botId // 账户ID
};
return msg;
应答消息回复 function节点(构造消息) 示例:
// 数据获取
let replyText = msg.message;
let requestId = msg.request_id;
msg.payload = {
"request_id": requestId, // 通信ID 获取数据流程中的
"response": replyText // 消息
};
return msg;
到此,我们已经完整的实现了一个关于”数据获取、数据处理、回复结果“的流程构造,而基于流程判断规则,分类规则,我们可以无限延申代码。
基于”数据处理“这个系统的大框架下,我们又可以实现,手动回复,自动推送,还有主动询问等等的高级流程。
聊聊:
就个人而言,搭建此系统较比搭建UI系统,不相上下,而此系统的易用性,功能性,相对而言也值得付出。
个人能力有限,文中内容起步较高,初学者建议先了解node-red的基础知识,请善用AI。
想要图中个别流程代码的友友,请留言,整体代码还在完善中,喜欢此类系统的友友可以留言探讨其他需要实现的功能。
码字不易,还望大家支持,谢谢!
|