小蜜陪护机器人 - 插件开发指南
概述
插件系统是小蜜陪护机器人的核心扩展机制,通过动态加载 DLL 的方式实现功能的灵活扩展,主要基于QT5.16开发。系统采用插件 + 工具脚本的双层架构:
- 插件层:用 C++ 编写的 DLL,封装底层功能(如天气查询、音乐播放、定时任务等)
- 工具层:用 JavaScript 编写的工具定义,将插件功能暴露给 Agent 调用
本文以 Weather 插件为例,详细讲解插件的编写、配置和使用流程。
一、插件在系统中的作用
1.1 架构定位
用户输入(语音/文字)
│
▼
TaskPlanner (任务规划专家)
│
▼
WorkerAgent (专业 Agent)
│
▼
ToolManager (工具管理器)
│
└──→ 执行 JavaScript 工具脚本
│
└──→ 调用 ScriptManager 中的全局对象
│
└──→ 插件暴露的 QObject 接口
│
└──→ 插件 DLL 内部实现
1.2 核心职责
| 职责 | 说明 |
|---|---|
| 功能封装 | 将复杂功能(网络请求、系统调用、硬件控制等)封装为独立模块 |
| 语言隔离 | C++ 实现核心逻辑,通过 QObject 桥接暴露给 JavaScript |
| 动态加载 | 支持运行时加载/卸载,无需重新编译主程序 |
| 热更新 | 插件更新后只需替换 DLL 文件即可生效 |
| 权限控制 | 通过插件接口统一管理外部访问权限 |
1.3 插件与系统的交互流程
┌─────────────────────────────────────────────────────────────────┐
│ 系统启动流程 │
├─────────────────────────────────────────────────────────────────┤
│ 1. 主程序启动 │
│ │ │
│ ▼ │
│ 2. PluginManager 扫描 bin/agentconfigs/Plugins/ 目录 │
│ │ │
│ ▼ │
│ 3. 加载所有 .dll 文件,调用 createPlugin() 创建插件实例 │
│ │ │
│ ▼ │
│ 4. 调用 plugin->initialize() 初始化插件 │
│ │ │
│ ▼ │
│ 5. 调用 plugin->scriptObject() 获取 QObject │
│ │ │
│ ▼ │
│ 6. 通过 ScriptManager 注册为全局对象(如 weatherController) │
│ │ │
│ ▼ │
│ 7. JS 工具脚本可直接调用全局对象 │
└─────────────────────────────────────────────────────────────────┘
二、插件接口规范
2.1 IPlugin 接口
所有插件必须实现 IPlugin 接口,定义于 [includes/iplugin.h]:
class IPlugin
{
public:
// 返回插件唯一名称(用于标识和注册)
virtual QString pluginName() const = 0;
// 返回插件版本号
virtual QString pluginVersion() const = 0;
// 返回插件描述
virtual QString pluginDescription() const = 0;
// 初始化插件,传入配置 JSON
virtual bool initialize(const QJsonObject &config) = 0;
// 关闭插件,释放资源
virtual void shutdown() = 0;
// 注入脚本执行器(由 PluginManager 调用)
// 插件 DLL 不能直接依赖 ScriptManager,因此通过函数指针注入
// executor 接受脚本内容,返回执行结果字符串
virtual void setScriptExecutor(std::function<QString(const QString&)> executor) { Q_UNUSED(executor); }
// 获取插件提供的 QObject 实例(用于注册到 ScriptManager)
virtual QObject* scriptObject() { return nullptr; }
// 返回脚本中暴露的全局对象名称
virtual QString scriptObjectName() const { return QString(); }
};
接口方法说明:
| 方法 | 必要性 | 说明 |
|---|---|---|
pluginName() |
必需 | 返回插件唯一标识名称 |
pluginVersion() |
必需 | 返回插件版本号 |
pluginDescription() |
必需 | 返回插件功能描述 |
initialize() |
必需 | 初始化插件,创建资源 |
shutdown() |
必需 | 关闭插件,释放资源 |
setScriptExecutor() |
可选 | 注入 JS 脚本执行能力,允许插件从 C++ 调用 JS |
scriptObject() |
可选 | 返回供 JS 调用的 QObject 实例 |
scriptObjectName() |
可选 | 返回 JS 全局变量名称 |
setScriptExecutor 的用途:
某些插件需要从 C++ 端执行 JavaScript 脚本,例如 TaskScheduler 插件在定时任务触发时需要执行用户预设的 JS 脚本。由于插件 DLL 不能直接依赖 ScriptManager(会导致循环依赖),系统通过 setScriptExecutor() 方法注入一个函数指针:
// TaskScheduler 使用示例
void TaskScheduler::executeTask(const QString& taskId)
{
ScheduledTask task = getTask(taskId);
// 通过注入的 executor 执行任务脚本
QString result = m_scriptExecutor(task.script);
emit taskCompleted(taskId, result);
}
Weather 插件不需要从 C++ 调用 JS,因此没有重写 setScriptExecutor() 方法。
2.2 插件导出函数
每个插件 DLL 必须导出两个 C 链接函数:
// 创建插件实例
extern "C" Q_DECL_EXPORT IPlugin* createPlugin();
// 销毁插件实例
extern "C" Q_DECL_EXPORT void destroyPlugin(IPlugin* plugin);
三、Weather 插件开发详解
3.1 项目结构
Plugins/weather/
├── CMakeLists.txt # CMake 构建配置
├── weatherplugin.h # 插件主类头文件
├── weatherplugin.cpp # 插件主类实现
├── weathercontroller.h # 天气控制器头文件(业务逻辑)
└── weathercontroller.cpp # 天气控制器实现
3.2 第一步:编写业务逻辑类
WeatherController 是实际执行天气查询的业务类,继承自 QObject:
// weathercontroller.h
class WeatherController : public QObject
{
Q_OBJECT
public:
// JS 可调用的方法,必须标记 Q_INVOKABLE
Q_INVOKABLE QVariant initLocation();
Q_INVOKABLE QVariant getTodayWeather();
Q_INVOKABLE QVariant getTomorrowWeather();
Q_INVOKABLE QVariant getWeatherByOffset(int dayOffset);
Q_INVOKABLE QVariant getAllWeather();
Q_INVOKABLE QString getCityName() const;
signals:
void locationReady(double lat, double lon, const QString &city);
void weatherReady();
void weatherError(const QString &error);
private:
// 内部实现方法(不暴露给 JS)
void fetchWeather();
QVariantMap buildDayWeather(int dayIndex);
static QString weatherCodeToText(int code);
};
关键要点:
- Q_OBJECT 宏:必须添加,启用 Qt 的元对象系统
- Q_INVOKABLE:标记需要暴露给 JavaScript 的方法
- 信号机制:用于异步回调(如位置就绪、天气数据就绪)
- 返回类型:使用
QVariant可以返回任意类型(字符串、数字、对象等)
3.3 第二步:实现业务逻辑
天气查询流程:
1. 调用 ip-api.com 获取本地经纬度(IP 定位)
│
▼
2. 使用经纬度从 Open-Meteo 获取 7 天天气预报
│
▼
3. 缓存天气数据(最高温、最低温、天气代码、风速、降水量、湿度)
│
▼
4. JS 调用查询方法时,从缓存中读取并格式化返回
核心实现片段:
// weathercontroller.cpp - 初始化位置
QVariant WeatherController::initLocation()
{
QNetworkRequest req(QUrl("http://ip-api.com/json/"));
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
m_net->get(req);
return QVariantMap{
{"success", true},
{"message", "Location request sent"}
};
}
// weathercontroller.cpp - 获取今天天气
QVariant WeatherController::getTodayWeather()
{
if (!m_weatherReady)
return QVariantMap{{"success", false}, {"error", "天气数据尚未加载"}};
return buildDayWeather(0);
}
// weathercontroller.cpp - 构建单日天气数据
QVariantMap WeatherController::buildDayWeather(int dayIndex)
{
QString date = m_dailyTime.at(dayIndex).toString();
double tMax = m_tempMax.at(dayIndex).toDouble();
double tMin = m_tempMin.at(dayIndex).toDouble();
int wCode = m_weatherCodes.at(dayIndex).toInt();
return QVariantMap{
{"success", true},
{"date", date},
{"tempMax", tMax},
{"tempMin", tMin},
{"weather", weatherCodeToText(wCode)},
{"humidity", m_humidity.at(dayIndex).toInt()},
{"windSpeed", m_windSpeed.at(dayIndex).toDouble()},
{"precipitation", m_precipitation.at(dayIndex).toDouble()},
};
}
3.4 第三步:编写插件封装类
WeatherPlugin 继承自 QObject 和 IPlugin,负责将 WeatherController 包装为标准插件:
// weatherplugin.h
class WeatherPlugin : public QObject, public IPlugin
{
Q_OBJECT
public:
QString pluginName() const override;
QString pluginVersion() const override;
QString pluginDescription() const override;
bool initialize(const QJsonObject &config) override;
void shutdown() override;
QObject* scriptObject() override;
QString scriptObjectName() const override;
private:
WeatherController *m_controller = nullptr;
};
插件实现:
// weatherplugin.cpp
bool WeatherPlugin::initialize(const QJsonObject &config)
{
Q_UNUSED(config);
// 创建业务逻辑实例
m_controller = new WeatherController(this);
// 异步初始化位置和天气
m_controller->initLocation();
m_initialized = true;
return true;
}
QObject* WeatherPlugin::scriptObject()
{
// 返回供 JS 调用的 QObject
return m_controller;
}
QString WeatherPlugin::scriptObjectName() const
{
// JS 中的全局变量名
return QStringLiteral("weatherController");
}
3.5 第四步:导出函数
// weatherplugin.cpp - 必须添加在文件末尾
extern "C" Q_DECL_EXPORT IPlugin* createPlugin()
{
return new WeatherPlugin();
}
extern "C" Q_DECL_EXPORT void destroyPlugin(IPlugin* plugin)
{
delete plugin;
}
3.6 第五步:CMake 配置
cmake_minimum_required(VERSION 3.16)
project(WeatherPlugin VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_AUTOMOC ON)
set(CMAKE_CXX_STANDARD 17)
# 输出到 bin/agentconfigs/Plugins/ 目录
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${AGENT_SOURCE_DIR}/bin/agentconfigs/Plugins")
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${AGENT_SOURCE_DIR}/bin/agentconfigs/Plugins")
# 查找 Qt 依赖
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Network)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Network)
# 源文件
set(PLUGIN_SOURCES
weathercontroller.h
weathercontroller.cpp
weatherplugin.h
weatherplugin.cpp
)
# 创建共享库
add_library(WeatherPlugin SHARED ${PLUGIN_SOURCES})
# Include 路径
target_include_directories(WeatherPlugin PRIVATE
${AGENT_SOURCE_DIR}/includes # 包含 iplugin.h
${CMAKE_CURRENT_SOURCE_DIR}
)
# 链接库
target_link_libraries(WeatherPlugin PRIVATE
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::Network
)
# MSVC: UTF-8 编码
if(MSVC)
target_compile_options(WeatherPlugin PRIVATE /utf-8)
endif()
关键配置:
| 配置项 | 说明 |
|---|---|
CMAKE_AUTOMOC ON |
自动运行 MOC 工具,处理 Q_OBJECT 宏 |
RUNTIME_OUTPUT_DIRECTORY |
DLL 输出目录,必须指向 bin/agentconfigs/Plugins/ |
AGENT_SOURCE_DIR |
由父 CMakeLists.txt 传入,指向项目根目录 |
四、工具脚本配置
插件开发完成后,需要编写对应的工具脚本供 Agent 调用。工具脚本定义于 bin/agentconfigs/tools/ 目录。
4.1 天气查询工具定义
天气查询.tool 文件内容:
{
"name": "天气查询",
"description": "查询天气预报,支持今天/明天/后天/近7天内任意日期的温度、湿度、阴晴、暴雨情况、风速及降水量",
"params": [
{
"type": "string",
"name": "查询日期",
"description": "要查询的日期类型:today(今天)、tomorrow(明天)、day_after_tomorrow(后天)、或具体日期如 2026-07-10"
}
],
"script_content": "function 天气查询() {\n var obj;\n try { obj = JSON.parse(params); } catch(e) { return JSON.stringify({success: false, result: '参数格式错误'}); }\n var queryDate = obj.查询日期 || 'today';\n if (typeof weatherController === 'undefined') return JSON.stringify({success: false, result: '天气服务未就绪'});\n var r;\n switch (queryDate) {\n case 'today': r = weatherController.getTodayWeather(); break;\n case 'tomorrow': r = weatherController.getTomorrowWeather(); break;\n case 'day_after_tomorrow': r = weatherController.getDayAfterTomorrowWeather(); break;\n default: r = weatherController.getWeatherByDate(queryDate); break;\n }\n r = JSON.parse(JSON.stringify(r));\n if (!r.success) return JSON.stringify({success: false, result: r.error || '查询失败'});\n var city = weatherController.getCityName();\n var desc = queryDate === 'today' ? '今天' : (queryDate === 'tomorrow' ? '明天' : (queryDate === 'day_after_tomorrow' ? '后天' : r.date));\n return JSON.stringify({\n success: true,\n city: city,\n result: city + ' ' + desc + '天气:' + r.weather + ',温度 ' + r.tempMin + '°C ~ ' + r.tempMax + '°C,湿度 ' + r.humidity + '%,风速 ' + r.windSpeed + ' km/h,降水量 ' + r.precipitation + ' mm。' + (r.isSevere ? ' ⚠️有暴雨/雷暴等恶劣天气,请注意安全!' : '') + (r.isRainy ? ' 有降雨,建议携带雨具。' : ''),\n date: r.date,\n weather: r.weather,\n tempMax: r.tempMax,\n tempMin: r.tempMin,\n avgTemp: r.avgTemp,\n humidity: r.humidity,\n windSpeed: r.windSpeed,\n precipitation: r.precipitation,\n isSevere: r.isSevere,\n isRainy: r.isRainy\n });\n}\n天气查询();"
}
4.2 工具脚本结构
| 字段 | 说明 |
|---|---|
name |
工具名称(中文,Agent 可见) |
description |
工具功能描述(供 LLM 理解何时调用) |
params |
参数列表,定义输入参数的类型、名称和描述 |
script_content |
JavaScript 脚本内容,实现工具逻辑 |
4.3 脚本编写要点
- 参数解析:从
params全局变量获取 JSON 格式的参数 - 全局对象调用:直接使用插件注册的全局对象(如
weatherController) - 返回格式:必须返回 JSON 字符串,包含
success字段 - 异常处理:捕获并返回错误信息,确保脚本不会崩溃
五、Agent 配置
5.1 天气专家 Agent 配置
天气专家.agent 文件定义了使用天气查询工具的 Agent:
{
"name": "天气专家",
"instructions": "你是一个天气预报专家,负责回答和天气相关的问题。查询日期参数用英文:today=今天, tomorrow=明天, day_after_tomorrow=后天。\n\n## 决策规则\n1. 用户询问具体某天天气 → 调用天气查询工具\n2. 用户询问多天天气 → 多次调用工具查询每一天\n3. 用户询问穿衣/出行建议 → 先查天气再给建议\n4. 用户只是闲聊天气无具体日期 → 直接输出最终答案,不调用工具\n\n## 记忆使用规则\n- 回答前先查看系统记忆中的【用户历史发言】,了解用户身份信息\n- 如果系统记忆中有用户之前提到过的常住城市或地点,且用户本次提问未指定地点,优先使用记忆中的地点进行查询\n- 如果记忆中有用户的名字或称呼,在最终答案中使用称呼让回复更亲切\n- 如果记忆中没有相关地点信息,按常规方式处理,不要编造\n\n## 输出格式(必须使用以下JSON格式,包含status字段)\n工具调用:{\"status\":\"tool_call\",\"tool_name\":\"天气查询\",\"arguments\":{\"查询日期\":\"today/tomorrow/day_after_tomorrow\"}}\n最终答案:{\"status\":\"done\",\"result\":\"天气信息及穿衣/出行建议文本\"}\n\n## 多步操作工作流程\n- 如果需要查询多天天气,可以多次调用工具\n- 完成所有查询后,汇总输出最终答案\n\n## 天气建议参考\n- <10°C:厚外套、毛衣;10-20°C:薄外套、长袖;20-30°C:短袖;>30°C:注意防暑\n- 有降雨:带雨具;暴雨/雷暴:减少外出;风速大:注意防风",
"tools": ["天气查询"]
}
配置字段说明:
| 字段 | 说明 |
|---|---|
name |
Agent 名称 |
instructions |
Agent 的角色定义和行为指令(System Prompt),包含决策规则、记忆使用规则、输出格式要求等 |
tools |
该 Agent 可以使用的工具列表 |
5.2 PluginManager 插件注册流程
插件加载时,PluginManager 会执行以下关键步骤:
// 1. 注入脚本执行能力给插件(允许插件从 C++ 调用 JS)
if (m_scriptManager) {
plugin->setScriptExecutor([sm](const QString &script) -> QString {
return sm->executeScript(script, "{}");
});
}
// 2. 将插件的脚本对象注册到 ScriptManager
if (m_scriptManager && plugin->scriptObject()) {
QString objName = plugin->scriptObjectName();
if (!objName.isEmpty()) {
m_scriptManager->registerGlobalObject(objName, plugin->scriptObject());
}
}
注册流程说明:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | setScriptExecutor() |
注入脚本执行函数,允许插件内部执行 JS 脚本 |
| 2 | scriptObject() |
获取插件提供的 QObject 实例 |
| 3 | scriptObjectName() |
获取全局对象名称 |
| 4 | registerGlobalObject() |
将 QObject 注册为 JS 全局变量 |
六、完整工作流程
6.1 从用户输入到结果返回
用户说:"明天天气怎么样?"
│
▼
语音识别(ASR)→ "明天天气怎么样?"
│
▼
TaskPlanner(任务规划专家)→ 分析意图,决定调用「天气专家」
│
▼
天气专家 Agent → 根据指令,决定使用「天气查询」工具
│
▼
ToolManager → 执行「天气查询」工具的 JavaScript 脚本
│
▼
ScriptManager → 脚本中调用全局对象 weatherController.getTomorrowWeather()
│
▼
WeatherController → 返回明天的天气数据
│
▼
工具脚本 → 格式化结果,返回 JSON
│
▼
天气专家 Agent → 将结果整理为自然语言回答
│
▼
语音合成(TTS)→ "明天成都多云,温度 25°C ~ 32°C,湿度 65%..."
│
▼
扬声器输出
七、插件开发最佳实践
7.1 代码规范
- 接口设计:对外暴露的方法使用
Q_INVOKABLE,内部方法不标记 - 错误处理:所有方法返回
QVariantMap,包含success和error字段 - 异步操作:耗时操作(如网络请求)使用信号机制,不阻塞主线程
- 内存管理:插件内部创建的对象使用
this作为父对象,自动清理
7.2 编译配置
- 输出目录:DLL 必须输出到
bin/agentconfigs/Plugins/ - Qt 依赖:按需引入 Qt 模块,避免不必要的依赖
- UTF-8 编码:MSVC 编译时添加
/utf-8选项
7.3 调试技巧
- 日志输出:使用
qDebug()和qWarning()输出调试信息 - 加载验证:检查 PluginManager 的加载日志,确认插件是否成功加载
- 脚本测试:在工具脚本中使用
printlog()输出调试信息
7.4 常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 插件加载失败 | DLL 输出目录不正确 | 检查 CMake 的 RUNTIME_OUTPUT_DIRECTORY 配置 |
| 找不到 createPlugin | 缺少导出函数 | 确保添加 extern "C" Q_DECL_EXPORT |
| JS 中找不到全局对象 | scriptObjectName() 返回空 |
检查 scriptObjectName() 实现 |
| 运行时崩溃 | 缺少 Qt 依赖 DLL | 将 Qt 相关 DLL 复制到 bin/ 目录 |
| 中文乱码 | 编码不一致 | 所有源文件使用 UTF-8 编码 |
八、其他插件参考
系统已内置以下插件,可作为开发参考:
| 插件 | 功能 | JS 全局对象 |
|---|---|---|
| TaskScheduler | Cron 定时任务调度 | taskScheduler |
| MusicPlayer | Kugou 音乐搜索播放 | musicPlayer |
| RadioTvPlayer | 广播和在线电视播放 | radioTvPlayer |
| VolumeControl | 系统音量控制 | volumeController |
| NetworkInfo | 网络连接详情查询 | networkInfo |
总结
插件系统是小蜜陪护机器人的核心扩展机制,通过以下设计实现高度可扩展性:
- C++ 插件层:封装底层功能,提供高性能实现
- QObject 桥接:通过 Qt 元对象系统实现 C++ 到 JavaScript 的无缝调用
- JavaScript 工具层:定义 Agent 可调用的工具接口,无需重新编译即可修改逻辑
- 动态加载:支持运行时扩展和热更新
开发者只需实现 IPlugin 接口,编写业务逻辑,即可为系统添加新功能,无需修改核心代码。
Weather插件代码下载地址: 百度网盘 请输入提取码 提取码:4qsj
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)