Add intent dispatch scaffold

This commit is contained in:
2026-06-01 18:35:28 +08:00
parent 0ee797e224
commit 4a7b739eea
9 changed files with 1343 additions and 4 deletions
+5
View File
@@ -28,6 +28,11 @@ qt_add_executable(QtDesktopPet
src/ai/GoogleGeminiProvider.cpp
src/ai/OpenAICompatibleProvider.h
src/ai/OpenAICompatibleProvider.cpp
src/assistant/CommandDispatcher.h
src/assistant/CommandDispatcher.cpp
src/assistant/IntentRouter.h
src/assistant/IntentRouter.cpp
src/assistant/UserIntent.h
src/character/AnimationClip.h
src/character/AnimationClip.cpp
src/character/CharacterPackage.h
File diff suppressed because it is too large Load Diff
+50
View File
@@ -0,0 +1,50 @@
#include "CommandDispatcher.h"
QString userIntentTypeName(UserIntentType type)
{
switch (type)
{
case UserIntentType::Chat:
return QStringLiteral("Chat");
case UserIntentType::Reminder:
return QStringLiteral("Reminder");
case UserIntentType::Weather:
return QStringLiteral("Weather");
case UserIntentType::FileOperation:
return QStringLiteral("FileOperation");
case UserIntentType::Search:
return QStringLiteral("Search");
}
return QStringLiteral("Unknown");
}
CommandDispatchResult CommandDispatcher::dispatch(const QString &text) const
{
const UserIntent intent = m_intentRouter.route(text);
if (intent.type == UserIntentType::Chat)
{
return {CommandDispatchAction::Chat, intent, intent.text};
}
return {CommandDispatchAction::UnsupportedTool, intent, unsupportedToolMessage(intent.type)};
}
QString CommandDispatcher::unsupportedToolMessage(UserIntentType type) const
{
switch (type)
{
case UserIntentType::Reminder:
return QStringLiteral("提醒功能尚未接入,我现在还不能创建本地提醒。");
case UserIntentType::Weather:
return QStringLiteral("天气查询功能尚未接入,我现在还不能查询实时天气。");
case UserIntentType::FileOperation:
return QStringLiteral("本地文件操作功能尚未接入。为避免风险,我现在不会读写或删除文件。");
case UserIntentType::Search:
return QStringLiteral("联网搜索功能尚未接入,我现在不会发起搜索请求。");
case UserIntentType::Chat:
return QString();
}
return QStringLiteral("该工具功能尚未接入。");
}
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include "IntentRouter.h"
#include <QString>
enum class CommandDispatchAction
{
Chat,
UnsupportedTool,
};
struct CommandDispatchResult
{
CommandDispatchAction action = CommandDispatchAction::Chat;
UserIntent intent;
QString message;
};
class CommandDispatcher
{
public:
CommandDispatchResult dispatch(const QString &text) const;
private:
QString unsupportedToolMessage(UserIntentType type) const;
IntentRouter m_intentRouter;
};
+123
View File
@@ -0,0 +1,123 @@
#include "IntentRouter.h"
#include <QStringList>
#include <QtGlobal>
namespace
{
bool containsAny(const QString &text, const QStringList &keywords)
{
for (const QString &keyword : keywords)
{
if (text.contains(keyword, Qt::CaseInsensitive))
{
return true;
}
}
return false;
}
bool isReminderIntent(const QString &text)
{
static const QStringList keywords = {
QStringLiteral("提醒"),
QStringLiteral("提醒我"),
QStringLiteral("叫我"),
QStringLiteral("闹钟"),
QStringLiteral("到点"),
};
return containsAny(text, keywords);
}
bool isFileOperationIntent(const QString &text)
{
static const QStringList keywords = {
QStringLiteral("文件"),
QStringLiteral("文件夹"),
QStringLiteral("目录"),
QStringLiteral("保存到"),
QStringLiteral("截图"),
QStringLiteral("打包"),
QStringLiteral("压缩"),
QStringLiteral("复制"),
QStringLiteral("备份"),
QStringLiteral("重命名"),
QStringLiteral("删除"),
QStringLiteral("移动"),
QStringLiteral("读取"),
QStringLiteral(".txt"),
QStringLiteral(".md"),
QStringLiteral("日志"),
};
return containsAny(text, keywords);
}
bool isWeatherIntent(const QString &text)
{
static const QStringList keywords = {
QStringLiteral("天气"),
QStringLiteral("气温"),
QStringLiteral("温度"),
QStringLiteral("冷不冷"),
QStringLiteral("热不热"),
QStringLiteral("下雨"),
QStringLiteral("降雨"),
QStringLiteral("带伞"),
QStringLiteral("刮风"),
QStringLiteral("风力"),
QStringLiteral("湿度"),
QStringLiteral("空气质量"),
QStringLiteral("雾霾"),
QStringLiteral("适合出门"),
QStringLiteral("穿什么"),
QStringLiteral("外面怎么样"),
};
return containsAny(text, keywords);
}
bool isSearchIntent(const QString &text)
{
static const QStringList keywords = {
QStringLiteral("搜索"),
QStringLiteral("搜一下"),
QStringLiteral("查一下"),
QStringLiteral("查找"),
QStringLiteral("联网"),
QStringLiteral("最新"),
QStringLiteral("官网"),
QStringLiteral("新闻"),
QStringLiteral("版本"),
QStringLiteral("文档"),
QStringLiteral("报错"),
};
return containsAny(text, keywords);
}
}
UserIntent IntentRouter::route(const QString &text) const
{
const QString trimmedText = text.trimmed();
if (isReminderIntent(trimmedText))
{
return {UserIntentType::Reminder, trimmedText};
}
if (isFileOperationIntent(trimmedText))
{
return {UserIntentType::FileOperation, trimmedText};
}
if (isWeatherIntent(trimmedText))
{
return {UserIntentType::Weather, trimmedText};
}
if (isSearchIntent(trimmedText))
{
return {UserIntentType::Search, trimmedText};
}
return {UserIntentType::Chat, trimmedText};
}
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include "UserIntent.h"
#include <QString>
class IntentRouter
{
public:
UserIntent route(const QString &text) const;
};
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include <QString>
enum class UserIntentType
{
Chat,
Reminder,
Weather,
FileOperation,
Search,
};
struct UserIntent
{
UserIntentType type = UserIntentType::Chat;
QString text;
};
QString userIntentTypeName(UserIntentType type);
+31 -4
View File
@@ -3,6 +3,7 @@
#include "../ai/AIProviderFactory.h"
#include "../ai/ConversationManager.h"
#include "../ai/ConversationStore.h"
#include "../assistant/CommandDispatcher.h"
#include "../character/CharacterPackageLoader.h"
#include "../character/CharacterPackageRepository.h"
#include "../config/ConfigManager.h"
@@ -460,19 +461,45 @@ bool PetWindow::submitChatMessage(const QString &message)
return false;
}
const QString trimmedMessage = message.trimmed();
if (trimmedMessage.isEmpty())
const QString normalizedMessage = message.trimmed();
if (normalizedMessage.isEmpty())
{
return false;
}
if (trimmedMessage.size() > MaxUserMessageLength)
if (normalizedMessage.size() > MaxUserMessageLength)
{
playState(QStringLiteral("error"), false);
showBubbleMessage(QStringLiteral("消息太长,请控制在 4000 字以内。"));
return false;
}
CommandDispatcher dispatcher;
const CommandDispatchResult result = dispatcher.dispatch(normalizedMessage);
if (result.action == CommandDispatchAction::UnsupportedTool)
{
playState(QStringLiteral("talk"), false);
showBubbleMessage(result.message);
return true;
}
return submitAiChatMessage(result.message);
}
bool PetWindow::submitAiChatMessage(const QString &message)
{
if (!m_conversationManager || m_conversationManager->isBusy())
{
showBubbleMessage(QStringLiteral("AI 回复正在进行。"));
return false;
}
const QString chatMessage = message.trimmed();
if (chatMessage.isEmpty())
{
return false;
}
ConfigManager configManager;
AIConfig config = configManager.loadAIConfig();
QString errorMessage;
@@ -507,7 +534,7 @@ bool PetWindow::submitChatMessage(const QString &message)
QPointer<PetWindow> window(this);
m_conversationManager->sendUserMessageStreaming(
trimmedMessage,
chatMessage,
[window](const QString &delta) {
if (!window.isNull())
{
+1
View File
@@ -60,6 +60,7 @@ private:
void addStateTestActions(QMenu *menu);
void startChat();
bool submitChatMessage(const QString &message);
bool submitAiChatMessage(const QString &message);
void clearConversation();
void cancelActiveAIRequest();
void showConversationHistory();