概述

插件系统是小蜜陪护机器人的核心扩展机制,通过动态加载 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);
};

关键要点:

  1. Q_OBJECT 宏:必须添加,启用 Qt 的元对象系统
  2. Q_INVOKABLE:标记需要暴露给 JavaScript 的方法
  3. 信号机制:用于异步回调(如位置就绪、天气数据就绪)
  4. 返回类型:使用 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 脚本编写要点

  1. 参数解析:从 params 全局变量获取 JSON 格式的参数
  2. 全局对象调用:直接使用插件注册的全局对象(如 weatherController
  3. 返回格式:必须返回 JSON 字符串,包含 success 字段
  4. 异常处理:捕获并返回错误信息,确保脚本不会崩溃

五、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 代码规范

  1. 接口设计:对外暴露的方法使用 Q_INVOKABLE,内部方法不标记
  2. 错误处理:所有方法返回 QVariantMap,包含 success 和 error 字段
  3. 异步操作:耗时操作(如网络请求)使用信号机制,不阻塞主线程
  4. 内存管理:插件内部创建的对象使用 this 作为父对象,自动清理

7.2 编译配置

  1. 输出目录:DLL 必须输出到 bin/agentconfigs/Plugins/
  2. Qt 依赖:按需引入 Qt 模块,避免不必要的依赖
  3. UTF-8 编码:MSVC 编译时添加 /utf-8 选项

7.3 调试技巧

  1. 日志输出:使用 qDebug() 和 qWarning() 输出调试信息
  2. 加载验证:检查 PluginManager 的加载日志,确认插件是否成功加载
  3. 脚本测试:在工具脚本中使用 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

总结

插件系统是小蜜陪护机器人的核心扩展机制,通过以下设计实现高度可扩展性:

  1. C++ 插件层:封装底层功能,提供高性能实现
  2. QObject 桥接:通过 Qt 元对象系统实现 C++ 到 JavaScript 的无缝调用
  3. JavaScript 工具层:定义 Agent 可调用的工具接口,无需重新编译即可修改逻辑
  4. 动态加载:支持运行时扩展和热更新

开发者只需实现 IPlugin 接口,编写业务逻辑,即可为系统添加新功能,无需修改核心代码。

Weather插件代码下载地址: 百度网盘 请输入提取码 提取码:4qsj

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐