
/*
 * Copyright (C) 2020, KylinSoft Co., Ltd.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
#include "msgpane.h"
#include "global/kaglobal.h"
#include "commandtemplatelist.h"
#include "setting/commandinstructionmodel.h"
#include "cmainwnd.h"
#include "./global/documentreader.h"
#include "configmanager.h"
#include "unixsocketwebsocket.h"

#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QDBusInterface>
#include <QDBusMessage>
#include <QDir>
#include <QFileDialog>
#include <QFileSystemWatcher>
#include <QHBoxLayout>
#include <QKeyEvent>
#include <QMap>
#include <QMessageBox>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QNetworkAccessManager>
#include <QEventLoop>
#include <QStandardPaths>
#include <QThread>
#include <QMetaObject>
#include <QVBoxLayout>
#include <QVariant>
#include <QSessionManager>
#include <QFutureWatcher>
#include <QtConcurrent/QtConcurrent>
#include <ukui-log4qt.h>
#include <ukuistylehelper/ukuistylehelper.h>
#include <kmessagebox.h>
#include <QClipboard>
#include <QMimeData>
#include <QDesktopServices>
#include <QDBusUnixFileDescriptor>
#include <stdio.h>
#include <QTextDocumentWriter>
#include <aiconfig/modelconfig.h>
#include <QTextDocument>
#include <QRegularExpression>
#include <QRegExp>
#include <QSet>
#include <QDate>
#include <QJsonDocument>
#include <QJsonObject>
#include "windowhelper.h"
#include "readercontrol.h"
#include "recordercontrol.h"
#include "global/voiceusingmanager.h"

#include "notificationnotify.h"
#include <QCoreApplication>
#include <QFileInfo>
#include <QUrl>


using namespace kyai::config::model;

namespace {

/** Markdown 转 HTML 前的标题规范化：
 *  - 模型偶发输出 7+ 个 #（如 ########），CommonMark 仅支持 1–6 级，多余 # 会原样显示；
 *  - 原全局 replace("#","##") 会把每个 # 倍增，导致 ## 变 ####、######## 变 16 个 #。
 *  做法：超长行首 ATX 压成 ##；仅「单独 # + 空白」的一级标题降为二级。
 */
QString normalizeAtxHeadingsForDisplay(QString msg)
{
    static const QRegularExpression longHeadingPattern(
        QStringLiteral(R"(^(\s{0,3})#{7,}(\s+))"),
        QRegularExpression::MultilineOption);
    msg.replace(longHeadingPattern, QStringLiteral("\\1##\\2"));

    static const QRegularExpression h1ToH2Pattern(
        QStringLiteral(R"(^(\s{0,3})#(?![#])(\s+))"),
        QRegularExpression::MultilineOption);
    msg.replace(h1ToH2Pattern, QStringLiteral("\\1##\\2"));
    return msg;
}

/** cmark 围栏代码块输出 <pre><code>…</code></pre>；Qt 富文本中 <pre> 默认不换行，长行会从右侧裁切。
 *  为未带 style 的 <pre …> 注入 pre-wrap，使代码在气泡宽度内折行显示。
 */
QString ensureCodeBlockPreTagsWrap(const QString &html)
{
    if (html.isEmpty()) {
        return html;
    }
    QString out = html;
    static const QRegularExpression preOpen(
        QStringLiteral(R"(<pre(?![^>]*\bstyle\s*=)([^>]*)>)"),
        QRegularExpression::CaseInsensitiveOption);
    out.replace(preOpen, QStringLiteral(R"(<pre style="white-space:pre-wrap;word-wrap:break-word"\1>)"));
    return out;
}

} // namespace

namespace {

QString expandHomePath(QString path)
{
    path = path.trimmed();
    if (path.startsWith(QStringLiteral("~/"))) {
        return QDir::homePath() + path.mid(1);
    }
    if (path == QStringLiteral("~")) {
        return QDir::homePath();
    }
    return path;
}

// 将 file:// 或相对路径转为本地绝对路径，供智能体文档上传/网关 [DOCUMENT] 使用
QString toAbsoluteLocalFilePath(const QString &pathOrUrl)
{
    QString local = expandHomePath(pathOrUrl);
    if (local.startsWith(QLatin1String("file:"), Qt::CaseInsensitive)) {
        local = QUrl(local).toLocalFile();
    }
    if (local.isEmpty()) {
        return local;
    }
    QFileInfo fi(local);
    const QString canonical = fi.canonicalFilePath();
    return canonical.isEmpty() ? fi.absoluteFilePath() : canonical;
}

// 智能体模式正常回答的角标
static QString modelMsgForTaskMode()
{
    return QStringLiteral("Content generated by AI");
}

static bool isVoiceAssistantSessionUuid(const QString &sessionUuid)
{
    return sessionUuid == QLatin1String("voice-assistant");
}

static bool isVoiceAssistantStoredMessage(const QJsonObject &obj)
{
    return obj.value(QStringLiteral("speechFlag")).toBool(false);
}

static QString taskSessionUuidForLoadedHistory(const QString &sessionId, const QString &sessionUuidInDb)
{
    if (sessionUuidInDb.isEmpty() || isVoiceAssistantSessionUuid(sessionUuidInDb)) {
        return sessionId;
    }
    return sessionUuidInDb;
}

void configureGatewaySocket(UnixSocketWebSocket &client)
{
    const QString socketPath = expandHomePath(
        ConfigManager::getInstance()->getConfigValue("gateway-socket-path").toString());
    if (!socketPath.isEmpty()) {
        client.setSocketPath(socketPath);
    }
}

QString configuredGatewayHost()
{
    QString gatewayHost =
        ConfigManager::getInstance()->getConfigValue("gateway-host").toString().trimmed();
    if (gatewayHost.isEmpty()) {
        gatewayHost = QStringLiteral("127.0.0.1:42617");
    }
    return gatewayHost;
}

UnixHttpResponse requestGatewayViaHttp(const QString &method,
                                       const QString &path,
                                       const QMap<QString, QString> &headers = QMap<QString, QString>(),
                                       const QByteArray &body = QByteArray())
{
    UnixHttpResponse resp;
    QNetworkAccessManager mgr;
    QNetworkRequest req;
    req.setUrl(QUrl(QStringLiteral("http://") + configuredGatewayHost() + path));
    req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
    for (auto it = headers.constBegin(); it != headers.constEnd(); ++it) {
        req.setRawHeader(it.key().toUtf8(), it.value().toUtf8());
    }

    QNetworkReply *reply = nullptr;
    if (method == QStringLiteral("GET")) {
        reply = mgr.get(req);
    } else if (method == QStringLiteral("POST")) {
        reply = mgr.post(req, body);
    } else if (method == QStringLiteral("DELETE")) {
        reply = mgr.deleteResource(req);
    } else {
        resp.errorString = QStringLiteral("Unsupported HTTP method: %1").arg(method);
        return resp;
    }

    QEventLoop loop;
    QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
    loop.exec();

    resp.statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
    resp.body = reply->readAll();
    if (reply->error() != QNetworkReply::NoError) {
        resp.errorString = reply->errorString();
    }
    reply->deleteLater();
    return resp;
}

UnixHttpResponse requestGatewayViaSocket(const QString &method,
                                         const QString &path,
                                         const QMap<QString, QString> &headers = QMap<QString, QString>(),
                                         const QByteArray &body = QByteArray())
{
    UnixSocketWebSocket client;
    configureGatewaySocket(client);
    if (method == QStringLiteral("GET")) {
        return client.httpGet(path, headers);
    }
    if (method == QStringLiteral("POST")) {
        return client.httpPost(path, body, headers);
    }
    if (method == QStringLiteral("DELETE")) {
        return client.httpDelete(path, headers);
    }

    UnixHttpResponse resp;
    resp.errorString = QStringLiteral("Unsupported HTTP method: %1").arg(method);
    return resp;
}

UnixHttpResponse requestGatewaySocketFirst(const QString &method,
                                           const QString &path,
                                           const QMap<QString, QString> &headers = QMap<QString, QString>(),
                                           const QByteArray &body = QByteArray())
{
    UnixHttpResponse resp = requestGatewayViaSocket(method, path, headers, body);
    if (resp.statusCode != 0) {
        return resp;
    }

    KyInfo() << "Gateway Unix socket request failed, fallback to HTTP:"
             << method << path << resp.errorString;
    return requestGatewayViaHttp(method, path, headers, body);
}

QString cleanBase64Whitespace(QString base64)
{
    base64.remove(QLatin1Char('\r'));
    base64.remove(QLatin1Char('\n'));
    base64.remove(QLatin1Char(' '));
    return base64;
}

QString mimeTypeFromImagePath(const QString &imagePath)
{
    const QString s = imagePath.toLower();
    if (s.endsWith(QLatin1String(".png")))
        return QStringLiteral("image/png");
    if (s.endsWith(QLatin1String(".jpg")) || s.endsWith(QLatin1String(".jpeg"))
        || s.endsWith(QLatin1String(".jpe")))
        return QStringLiteral("image/jpeg");
    if (s.endsWith(QLatin1String(".webp")))
        return QStringLiteral("image/webp");
    if (s.endsWith(QLatin1String(".gif")))
        return QStringLiteral("image/gif");
    if (s.endsWith(QLatin1String(".bmp")))
        return QStringLiteral("image/bmp");
    return QStringLiteral("image/png");
}

// 与 voice-assistant GatewayChatClient::encodeContentWithScreenshotImage 中 data URI 规则一致，再包一层 [IMAGE:...]；供任务模式发给网关。
QString taskModeImageDataUri(const QString &imageSrc, const QString &imagePath)
{
    const QString trimmed = imageSrc.trimmed();
    if (trimmed.isEmpty())
        return QString();

    if (trimmed.startsWith(QLatin1String("data:image/"), Qt::CaseInsensitive)) {
        const int comma = trimmed.indexOf(QLatin1Char(','));
        if (comma <= 0)
            return trimmed;
        const QString header = trimmed.left(comma);
        QString payload = cleanBase64Whitespace(trimmed.mid(comma + 1));
        if (payload.isEmpty())
            return QString();
        return header + QLatin1Char(',') + payload;
    }

    const QString payload = cleanBase64Whitespace(trimmed);
    if (payload.isEmpty())
        return QString();
    return QStringLiteral("data:%1;base64,").arg(mimeTypeFromImagePath(imagePath)) + payload;
}

QString prependTaskModeImageForGateway(const QString &userText, const QString &imageSrc, const QString &imagePath)
{
    const QString dataUri = taskModeImageDataUri(imageSrc, imagePath);
    if (dataUri.isEmpty())
        return userText;
    // 标记置于用户输入之前，两段之间两个换行（与产品约定、voice-assistant [IMAGE:...] 格式一致）
    const QString marker = QStringLiteral("[IMAGE:") + dataUri + QLatin1Char(']');
    return marker + QStringLiteral("\n\n") + userText;
}

// 任务模式：上传的文档在用户输入前增加 [DOCUMENT] 块，与用户原文之间两个换行
QString prependTaskModeDocumentsForGateway(const QString &userText, const QList<DocumentInfo> &documents)
{
    if (documents.isEmpty())
        return userText;

    QString blocks;
    for (const DocumentInfo &doc : documents) {
        const QString name = doc.documentName.trimmed();
        const QString path = toAbsoluteLocalFilePath(doc.documentPath);
        if (name.isEmpty() && path.isEmpty())
            continue;
        if (!blocks.isEmpty())
            blocks += QLatin1Char('\n');
        blocks += QStringLiteral("[DOCUMENT]\n  name=%1\n  path=%2\n  [/DOCUMENT]")
                        .arg(name, path);
    }
    if (blocks.isEmpty())
        return userText;
    return blocks + QStringLiteral("\n\n") + userText;
}

} // namespace

struct ClipboardImagePayload
{
    QString mimeType;   // e.g. image/png
    QByteArray data;   // raw image bytes (png/jpeg/webp...)
};

CMsgPane::CMsgPane()
{
    createAllChildWnd();
    initChatOperator();
    KyInfo() << "end initChatOperator";
    initSolts();
    initPluginList();
    initCommandList();
    initLoadingTimer();
    initFollowUpManager();
    KyInfo() << "end initCommandList";
}

void CMsgPane::createAllChildWnd()
{
    m_database = ChatDatabase::getChatDatabase();
#ifdef KB_SUPPORT_ENABLED
    m_knowledgeBaseDateBase = KnowledgeBaseDatabase::getInstance();
#endif
}

void CMsgPane::initSolts()
{
    // 会议助手功能已移除，相关连接已删除

    //文生图
    connect(m_chat, &SystemChat::sigPictureUpdate, this, &CMsgPane::updateToShowPic);

    //图像处理
    connect(m_chat, &SystemChat::pictureOperateUpdate, this, &CMsgPane::updateToShowOperatePic);

    //系统助手-信息来源
    connect(m_chat, &SystemChat::sigInfoSourceUpdate, this, &CMsgPane::updateToShowSource);
    //系统助手-联网搜索信息来源
    connect(m_chat, &SystemChat::sigWebSearchSourceUpdate, this, &CMsgPane::updateToShowWebSearchSource);

    // 对话超时处理
    // 超时处理已移至前端 QML 端

    //数据埋点
    //connect(m_channelMsg, &ChannelMessage::sigSendDataCollect, this, &CMsgPane::onSendDataCollect);

    //配置界面
    //connect(m_channelMsg, &ChannelMessage::sigShowSettingPage, [this]() {
    //    Q_EMIT sigSetup();
    //});

    //关机：执行完整销毁；休眠：仅暂停、保留会话以便唤醒后带上下文续聊
    QDBusConnection::systemBus().connect(
        QString("org.freedesktop.login1"), QString("/org/freedesktop/login1"), QString("org.freedesktop.login1.Manager"), QString("PrepareForShutdown"), this, SLOT(onPrepareForShutdown(bool)));
    QDBusConnection::systemBus().connect(
        QString("org.freedesktop.login1"), QString("/org/freedesktop/login1"), QString("org.freedesktop.login1.Manager"), QString("PrepareForSleep"), this, SLOT(onPrepareForSleep(bool)));

    //会议助手dbus锁屏限制接口
    if(m_meetingInhibitLockScreen == nullptr){
        m_meetingInhibitLockScreen = new QDBusInterface("org.gnome.SessionManager", "/org/gnome/SessionManager","org.gnome.SessionManager",QDBusConnection::sessionBus());
    }
    QDBusConnection::systemBus().connect(QString("org.freedesktop.login1"),
                                             QString("/org/freedesktop/login1"),
                                             QString("org.freedesktop.login1.Manager"),
                                             QString("PrepareForShutdown"), this,
                                                    SLOT(onMeetingRunningPrevent()));

}

void CMsgPane::initChatOperator()
{
    m_chat = SystemChat::getPtr();
    // 会议助手功能已移除
#ifdef KB_SUPPORT_ENABLED
    m_KBChat = KnowledgeBase::getPtr();
    // 初始化系统指南知识库
    m_KBChat->initForSystemGuide();
#endif

    connect(m_chat, &SystemChat::reply, this, &CMsgPane::onRecvMsg);
    connect(m_chat, &SystemChat::toolReply, this, &CMsgPane::onRecvTool);
    // 会议助手功能已移除，相关连接已删除

    m_taskSessionManager = new TaskSessionManager(this);
    connect(m_taskSessionManager, &TaskSessionManager::reply,
            this, &CMsgPane::onRecvTaskMsg);
    connect(m_taskSessionManager, &TaskSessionManager::sessionCreated,
            this, [this]() { Q_EMIT taskSessionCountChanged(m_taskSessionManager->sessionCount()); });
    connect(m_taskSessionManager, &TaskSessionManager::sessionClosed,
            this, [this]() { Q_EMIT taskSessionCountChanged(m_taskSessionManager->sessionCount()); });

    connect(NotificationNotify::getInstance(), &NotificationNotify::viewClicked,
            this, [this](const QString &sessionId) {
        if (sessionId.isEmpty())
            return;
        Q_EMIT sigActivateAndSwitchToSession(sessionId);
        loadHistoryBySessionId(sessionId);
    });

    connect(m_chat, &SystemChat::sigGeneratingPic, [=](QString model, QString feedbackURL){
        QJsonObject jsonObj;
        jsonObj["drawMsg"] = true;
        jsonObj["modelMsg"] = model;
        jsonObj["feedbackURL"] = feedbackURL;
        jsonObj["operatorId"] = m_operatorId;
        QJsonDocument jDoc(jsonObj);
        QString jsonStr = jDoc.toJson(QJsonDocument::Indented);
        pushMsg("updateMsg", jsonStr, tr("Answering"));
    });

    connect(m_chat, &SystemChat::sigInitCompleted, this, &CMsgPane::onChatInited, Qt::QueuedConnection);

    connect(m_chat, &SystemChat::sigNLPModelChanged, [&](){
        //更新提示词，更新上传图片和文档按钮
        m_commandListModel->initData(m_chat->getPrompts());
        Q_EMIT sigNlpModelInitCompleted();

    });

#ifdef KB_SUPPORT_ENABLED
    connect(m_KBChat, &KnowledgeBase::reply, this, &CMsgPane::onKBRecvMsg);
#endif

    return;
}

void CMsgPane::onChatInited()
{
    KyInfo() << "Chat init Completed";

    Q_EMIT chatInitCompleted();

    m_chat->setNLPModel(ConfigManager::getInstance()->getConfigValue("cur-nlp-model").toString());
    initNLPModelList();

    // SystemChat 初始化完成后，尝试从旧系统迁移指令
    CommandInstructionModel::getInstance()->migrateFromOldSystem();

    return;
}
QString CMsgPane::getMeetingSpeechCurModel()
{
    ModelConfig mc;
    std::string modelStr = mc.getCurrentModelName(AiCapability::Speech, DeployType::PublicCloud);
    QString meetingSpeechString = QString::fromStdString(modelStr);
    return meetingSpeechString;
}

void CMsgPane::initNLPModelList()
{
    QList<ModelItem * > temp = m_chat->getNLPModelList();
    for (auto item : temp){
        qDebug() << "******* name :" << item->m_modelName << "     ischecked:" << item->m_isChecked;
    }
    ModelItemListModel::getInstance()->setItems(temp);

    return;
}


QString CMsgPane::getNLPCurModel()
{

    return m_chat->getNLPCurModel();
}

void CMsgPane::setNLPModel(QString name)
{
    if(name == tr("System Default"))
    {
        name = "";
    }
    ConfigManager::getInstance()->updateConfig("cur-nlp-model", name);
    m_chat->setNLPModel(name);
#ifdef KB_SUPPORT_ENABLED
    if(m_KBChat != nullptr) {
        m_KBChat->setModel(name);
    }
#endif
    return;
}

void CMsgPane::initNLPModel()
{
    QString modelName = ConfigManager::getInstance()->getConfigValue("cur-nlp-model").toString();
    KyInfo()<< "程序初始化设置模型名：" << modelName;
    setNLPModel(modelName);
}

void CMsgPane::stopLoadTimer()
{
    m_loadTimer->stop();

    return;
}

bool CMsgPane::getIsSidebarClose()
{
    return ConfigManager::getInstance()->getConfigValue("is-sidebar-close").toBool();
}

void CMsgPane::setIsSidebarClose(bool flag)
{
    ConfigManager::getInstance()->updateConfig("is-sidebar-close", flag);
    return;
}

void CMsgPane::setWindowProperty(QObject *win)
{
    if (win == nullptr) return;

    QWindow * curWin = qobject_cast<QWindow*>(win);
    if (curWin) {
#ifdef UKUI_WINDOWHELPER_SUPPORT
        UkuiWindowHelper *winHelper = new UkuiWindowHelper(curWin);
        winHelper->setDecorationCompoents(UkuiWindowHelper::DecorationComponent(UkuiWindowHelper::DecorationComponent::Shadow) |
                                          UkuiWindowHelper::DecorationComponent(UkuiWindowHelper::DecorationComponent::Border) |
                                          UkuiWindowHelper::DecorationComponent(UkuiWindowHelper::DecorationComponent::RoundCorner));
#endif
    }

    return;
}

void CMsgPane::setKnowledgeBaseStatus(bool isActive)
{
    ConfigManager::getInstance()->updateConfig("is-knowledgeBase-active", isActive);

    return;
}

bool CMsgPane::getIsReplying()
{
    return m_isReplying;
}

void CMsgPane::applyCommandFromSelection(const QString &uuid)
{
    qDebug() << "[DEBUG-CPP] applyCommandFromSelection 被调用, uuid:" << uuid;
    Q_EMIT sigApplyCommandFromSelection(uuid);
    qDebug() << "[DEBUG-CPP] sigApplyCommandFromSelection 信号已发射";
}

void CMsgPane::enterFromWordSelection()
{
    Q_EMIT sigEnterFromWordSelection();
}

void CMsgPane::onSetCloudModel(AiCapability capability, QString modelName)
{
    Q_UNUSED(modelName);
    Q_UNUSED(capability);
}


void CMsgPane::onEnterMeetingRecordPage(bool entered)
{
    Q_EMIT sigEnterMeetingRecordPage(entered);
}

void CMsgPane::onSwitchToPlugin(QString page)
{
    pushMsg("switchToPlugin", "", page);
}

void CMsgPane::onClearContext()
{
    m_chat->clearContext();

    return;
}

void CMsgPane::onClearKBContext()
{
    // 清除知识库历史上下文
#ifdef KB_SUPPORT_ENABLED
    if(m_KBChat != nullptr){
        KyInfo() << "清除知识库历史上下文";
        m_KBChat->clearKBChatHistory();
        m_KBChat->initKBChatHistory();
    }
#endif

    return;
}

void CMsgPane::setDeepThinking(bool checked)
{
    m_enable_thinking = checked;

    return;
}

void CMsgPane::setWebSearch(bool checked)
{
    m_web_search = checked;

    return;
}

QString CMsgPane::onSendMsg(const QString &jsonMsgHead, const QString &msg)
{
    m_isUserStopped = false;
    
    QString chatType;
    // 知识库激活状态
    bool activeKB = false;
    //解析jsonMsgHead
    QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonMsgHead.toUtf8());
//    KyInfo() << "onSendMsg" << jsonDoc;

    if (jsonDoc.isNull() || !jsonDoc.isObject()) {
        QJsonObject innerErrorJsonObject;
        innerErrorJsonObject["isReplying"] = false;
        QJsonDocument innerErrorJsonDoc(innerErrorJsonObject);
        return innerErrorJsonDoc.toJson(QJsonDocument::Indented);
    }

    QJsonObject jsonObjToQt = jsonDoc.object();
    msgHead = jsonObjToQt["msgHead"].toString();
    promptId = jsonObjToQt["promptId"].toInt();
    promptIconPath = jsonObjToQt["promptIconPath"].toString();
    commandUuid = jsonObjToQt["commandUuid"].toString();    //解析commandUuid(新指令系统使用)
    bool reAnswer = jsonObjToQt["reAnswer"].toBool();
    // 请求级标记：本次请求是否是"生成待办"。统一在此计算，让 !reAnswer 分支的用户消息头和后续 Bot 占位消息共用
    const QString generateTodoUuid = "550e8400-e29b-41d4-a716-446655440001";
    const bool isGenerateTodoRequest = (commandUuid == generateTodoUuid);
    imagePath = jsonObjToQt["imagePath"].toString();
    imageSrc = jsonObjToQt["imageSrc"].toString();
    QString targetDocument = jsonObjToQt["targetDocument"].toString();
    int modeType = jsonObjToQt["modeType"].toInt(0);

    // 解析文档数组
    m_documents.clear();
    if (jsonObjToQt.contains("document")) { // 检查是否存在该字段
        QJsonObject docmentObj = jsonObjToQt["document"].toObject();
        chatType = docmentObj["type"].toString();

        qDebug() << "\n文件数据解析结果：";
        qDebug() << "文件类型:" << chatType;

        if (chatType == "knowledgeBase"){
            activeKB = true;
        } else {
            // 解析文件列表数组（list 数组）
            QJsonArray docListArray = docmentObj["list"].toArray();
            qDebug() << "文件总数:" << docListArray.size();

            // 遍历文件列表
            for (int i = 0; i < docListArray.size(); ++i) {
                QJsonObject docItemObj = docListArray[i].toObject();
                DocumentInfo doc;
                doc.documentName = docItemObj.value("documentName").toString();
                doc.documentSize = docItemObj.value("documentSize").toString();
                doc.documentIcon = docItemObj.value("documentIcon").toString();
                doc.documentPath = toAbsoluteLocalFilePath(docItemObj.value("documentPath").toString());
                doc.documentUuid = docItemObj.value("documentUuid").toString();
                doc.isDir = docItemObj.value("isDir").toBool();
                doc.childCount = docItemObj.value("childCount").toInt();

                m_documents.append(doc);
            }

            if (chatType == "file") {
                activeKB = false;
            } else if (chatType == "knowledgeBaseFile") {
                activeKB = true;
            }
        }
    }

    m_chat->soundFlag = jsonObjToQt["speechFlag"].toBool();

    send_message = msg;

    m_chat->target = RECVMSG;
    m_operatorId = 0; // 发送消息将图像处理操作置为文生图

    bool flag = false; //识别是否为系统控制命令

    // 标识是否来自划词
    bool isFromSelection = false;
    QString selectPrompt = "";
    if (jsonObjToQt.contains("isFromSelection")) {
        isFromSelection = jsonObjToQt["isFromSelection"].toBool();
    }
    if (jsonObjToQt.contains("selectPrompt")) {
        selectPrompt = jsonObjToQt["selectPrompt"].toString();
    }

    QJsonObject retJsonObject;
    retJsonObject["isReplying"] = true;
    QJsonDocument retJsonDoc(retJsonObject);
    QString retJsonString = retJsonDoc.toJson(QJsonDocument::Indented);

    // 发送信号通知 QML 端即将发送消息，检查并更新 System Default 按钮状态
    Q_EMIT sigAboutToSendMessage();

    //判断是否有标签模板
    // 系统指南(promptId==4)使用知识库SDK，不使用SystemChat::setActor
    // 如果使用新的指令系统（commandUuid不为空），则不需要调用setActor
    if(msgHead != "" && promptId != -1 && promptId != 4 && commandUuid.isEmpty()) {
        m_chat->setActor(promptId);
        KyInfo() << "set actor: " << promptId;
    }

    // 重新回答不需要渲染右侧消息
    if(!reAnswer) {
        QJsonObject jsonObjForSendMsg;
        jsonObjForSendMsg["msgHead"] = msgHead;
        jsonObjForSendMsg["promptId"] = promptId;
        jsonObjForSendMsg["promptIconPath"] = promptIconPath;
        jsonObjForSendMsg["colorMsg"] = "";
        jsonObjForSendMsg["imagePath"] = imagePath;
        jsonObjForSendMsg["imageSrc"] = imageSrc;
        jsonObjForSendMsg["fileType"] = chatType;
        jsonObjForSendMsg["commandUuid"] = commandUuid;

        QJsonArray docsArray;
        // 遍历文档列表，逐个转换为 JSON 对象
        for (const DocumentInfo& doc : m_documents) {
         QJsonObject docObj;
         // 将 DocumentInfo 的属性添加到 JSON 对象中
         docObj["name"] = doc.documentName;
         docObj["size"] = doc.documentSize;
         docObj["icon"] = doc.documentIcon;
         docObj["path"] = doc.documentPath;
         docObj["uuid"] = doc.documentUuid;
         docObj["isDir"] = doc.isDir;
         docObj["childCount"] = doc.childCount;

         // 将 JSON 对象添加到数组中
         docsArray.append(docObj);
        }

        jsonObjForSendMsg["documents"] = docsArray;
        jsonObjForSendMsg["speechFlag"] = m_chat->soundFlag;
        jsonObjForSendMsg["targetDocument"] = targetDocument;
        jsonObjForSendMsg["modeType"] = modeType;
        if (modeType == 1) {
            m_chatType = TASK_MODE;
            QString activeUuid = m_taskSessionManager->getActiveSessionUuid();
            if (activeUuid.isEmpty()) {
                int historyId = getCurrentMaxHistoryId();
                const QString historyIdStr = QString::number(historyId);
                if (m_database->sessionExists(historyIdStr)
                        && isVoiceAssistantSessionUuid(m_database->selectSessionUuidBySessionID(historyIdStr))) {
                    historyId = getNextHistoryId();
                }
                activeUuid = m_taskSessionManager->createSession(QString(), historyId);
                if (!activeUuid.isEmpty()) {
                    Q_EMIT changeTaskSessionId(activeUuid);
                    m_chat->setSession(historyId);
                    Q_EMIT changeSessionId(historyId);
                }
            }
        }
        if (imagePath.length() == 0 && activeKB && !isFromSelection) {
            jsonObjForSendMsg["chatType"] = KNOWLEDGEBASE;
            m_chatType = KNOWLEDGEBASE;
        }
        // 系统指南(promptId==4或使用新指令系统的系统指南UUID)也使用知识库类型，以便正确渲染
        QString systemGuideUuid = "550e8400-e29b-41d4-a716-446655440002";
        if (promptId == 4 || commandUuid == systemGuideUuid) {
            jsonObjForSendMsg["chatType"] = KNOWLEDGEBASE;
            m_chatType = KNOWLEDGEBASE;
        }

        // 生成待办标记（请求级，重答路径不会进入此分支，但 Bot 占位消息会用同一变量）
        if (isGenerateTodoRequest) {
            jsonObjForSendMsg["isGenerateToDo"] = true;
        }

        // 划词标记划词字段
        if (isFromSelection) {
            jsonObjForSendMsg["isfromSelection"] = isFromSelection;
        }
        QJsonDocument aiJsonSendWebDoc(jsonObjForSendMsg);
        QString jsonMsgHeadToWeb = aiJsonSendWebDoc.toJson(QJsonDocument::Indented);
        pushMsg("addMsg", jsonMsgHeadToWeb, msg);
    }

    // 判断知识库是否启动并且初始化成功，不成功，发送报错消息，返回
#ifdef KB_SUPPORT_ENABLED
    if (activeKB && m_KBChat != nullptr && m_KBChat->getError() != static_cast<int>(AI_COMMON_SUCCESS)){
        QTimer *tempTimer = new QTimer(this);
        connect(tempTimer,&QTimer::timeout,this,[=]{
                QJsonObject jsonObj;
                jsonObj["isFirst"] = true;
                jsonObj["modelMsg"] = "ai";
                jsonObj["colorMsg"] = "red";
                QJsonDocument jDoc(jsonObj);
                QString jsonStr = jDoc.toJson(QJsonDocument::Indented);
                QString kbInitErrorMsg = tr("The initialization of the knowledge base failed. Please restart the assistant and try again.");
                pushMsg("addRecvMsg", jsonStr, kbInitErrorMsg);
                pushMsg("replyEnd", true);
                clearJsonMessage();
                tempTimer->stop();
        });
        tempTimer->start(100);
        m_chatType = NORMAL;
        return retJsonString;
    }
#else
    if (activeKB){
        QTimer *tempTimer = new QTimer(this);
        connect(tempTimer,&QTimer::timeout,this,[=]{
                QJsonObject jsonObj;
                jsonObj["isFirst"] = true;
                jsonObj["modelMsg"] = "ai";
                jsonObj["colorMsg"] = "red";
                QJsonDocument jDoc(jsonObj);
                QString jsonStr = jDoc.toJson(QJsonDocument::Indented);
                QString kbNotSupportedMsg = tr("Knowledge base is not supported on this platform.");
                pushMsg("addRecvMsg", jsonStr, kbNotSupportedMsg);
                pushMsg("replyEnd", true);
                clearJsonMessage();
                tempTimer->stop();
        });
        tempTimer->start(100);
        return retJsonString;
    }
#endif

    QJsonObject jsonObj;
    jsonObj["isFirst"] = true;
    jsonObj["isGenerateToDo"] = isGenerateTodoRequest;

    QJsonDocument jDoc(jsonObj);
    QString jsonStr = jDoc.toJson(QJsonDocument::Indented);
    if(activeKB){
        pushMsg("addRecvMsg", jsonStr, tr("Searching the knowledge base"));
    }else{
        pushMsg("addRecvMsg", jsonStr, tr("Answering"));
    }

    if(flag)
    {
        QString tmpMsg = msg;
        QString punctuation = ".,;:!？。";
        for(int i = 0; i < punctuation.length(); ++i) // 删除标点符号
            tmpMsg.remove(punctuation[i]);
        jsonObjToWeb["modelMsg"] = "ai";
        QJsonDocument aiJsonSendWebDoc(jsonObjToWeb);
        QString jsonMsgHeadToWeb = aiJsonSendWebDoc.toJson(QJsonDocument::Indented);
        pushMsg("updateMsg", jsonMsgHeadToWeb, tr("Okay, it's done for you: ") + tmpMsg);
        QTimer *tempTimer = new QTimer(this);
        connect(tempTimer,&QTimer::timeout,this,[&]{
            pushMsg("replyEnd", true);
        });
        tempTimer->setSingleShot(true);
        tempTimer->start(1000);
        clearJsonMessage();
        return retJsonString;
    }

    // 真正的发送时机
#ifdef KB_SUPPORT_ENABLED
    if (activeKB){
        // 知识库问答， 如果携带文件，针对指定文件问答；不携带文件，针对整个知识库问答。
        if(m_KBChat != nullptr) {
            QStringList docList;
            if (m_documents.count() != 0) {
                for (auto doc : m_documents){
                    if (doc.isDir) {
                        QStringList list = getAllFileIdsByFilepath(doc.documentPath);
                        docList += list;
                    } else {
                        docList.append(doc.documentUuid);
                    }
                }

                m_KBChat->setDocIds(docList);
            } else {
                m_KBChat->setDocIds(docList);
            }

            m_KBChat->sendKBQuestion(msg);
            m_KBChat->kbChatHistoryAddQuestion(msg); // 知识库上下文添加问题
            m_chatType = KNOWLEDGEBASE; // 设置为知识库类型，以便使用知识库的回调处理
        }
    } else if (promptId == 4) {
        // 系统指南(promptId==4)使用知识库SDK，知识库Id传空字符串
        if(m_KBChat != nullptr) {
            m_KBChat->sendSystemGuideQuestion(msg);
        }
        m_chatType = KNOWLEDGEBASE; // 设置为知识库类型，以便使用知识库的回调处理
#else
    if (activeKB){
        // 知识库功能不支持，发送错误消息
        QJsonObject jsonObj;
        jsonObj["isFirst"] = true;
        jsonObj["modelMsg"] = "ai";
        jsonObj["colorMsg"] = "red";
        QJsonDocument jDoc(jsonObj);
        QString jsonStr = jDoc.toJson(QJsonDocument::Indented);
        QString kbNotSupportedMsg = tr("Knowledge base is not supported on this platform.");
        pushMsg("addRecvMsg", jsonStr, kbNotSupportedMsg);
        pushMsg("replyEnd", true);
        clearJsonMessage();
        m_chatType = NORMAL;
        return retJsonString;
    } else if (promptId == 4) {
        // 系统指南不支持
        m_chatType = NORMAL;
#endif
    } else if (modeType == 1) {
        QString activeUuid = m_taskSessionManager->getActiveSessionUuid();
        if (activeUuid.isEmpty()) {
            // 活跃会话已被清理（如智能体回答过程中系统休眠/睡眠唤醒后会话被关闭），
            // 依据当前 historyId 恢复或重建任务会话，避免重新回答时报“会话创建失败”。
            // 注意：historyId 可能为 0（首个会话使用当前最大 id 而非 +1），此时同样需要重建会话。
            int historyId = m_chat->getSession();
            if (historyId <= 0) {
                historyId = getCurrentMaxHistoryId();
            }
            const QString historyIdStr = QString::number(historyId);
            const QString sessionUuidInDb = m_database->selectSessionUuidBySessionID(historyIdStr);
            const QString uuid = taskSessionUuidForLoadedHistory(historyIdStr, sessionUuidInDb);
            TaskSession *existing = uuid.isEmpty() ? nullptr : m_taskSessionManager->getSession(uuid);
            if (existing) {
                m_taskSessionManager->activateSession(uuid);
                activeUuid = uuid;
            } else {
                activeUuid = m_taskSessionManager->createSession(uuid, historyId);
            }
            if (!activeUuid.isEmpty()) {
                m_chat->setSession(historyId);
                Q_EMIT changeTaskSessionId(activeUuid);
                Q_EMIT changeSessionId(historyId);
            }
        }
        if (activeUuid.isEmpty()) {
            errorMessageToJs("", "ai", "red", "会话创建失败");
            clearJsonMessage();
            return retJsonString;
        }
        QString taskContent = msg;
        if (!m_documents.isEmpty())
            taskContent = prependTaskModeDocumentsForGateway(taskContent, m_documents);
        taskContent = prependTaskModeImageForGateway(taskContent, imageSrc, imagePath);
        m_taskSessionManager->sendMessage(activeUuid, taskContent);
        m_chatType = TASK_MODE;
        new_reply = true;
        TaskSession* activeSession = m_taskSessionManager->getSession(activeUuid);
        QString historyIdStr = activeSession ? QString::number(activeSession->historyId) : activeUuid;
        Q_EMIT sessionReplyStatusChanged(historyIdStr, true, false);
    } else if (imagePath.length() != 0) {
        m_chat->sendMessage(msg, imagePath, m_web_search, m_enable_thinking);
    } else if (m_documents.count() != 0 && commandUuid.isEmpty()) {
        // 只有在未选择新指令系统（commandUuid 为空）时，才按文档问答处理
        QString documentPath = m_documents[0].documentPath;
        // 检查文件是否存在
        QFileInfo fileInfo(documentPath);
        if (!fileInfo.exists()){
            QTimer *tempTimer = new QTimer(this);
            connect(tempTimer,&QTimer::timeout,this,[=]{
                    QJsonObject jsonObj;
                    jsonObj["modelMsg"] = "ai";
                    jsonObj["colorMsg"] = "red";
                    jsonObj["isEnd"] = true;
                    QJsonDocument jDoc(jsonObj);
                    QString jsonStr = jDoc.toJson(QJsonDocument::Indented);
                    QString tempErrorMsg = tr("There are deleted files in the selected file. Please select again and try again.");
                    pushMsg("updateMsg", jsonStr, tempErrorMsg);
                    pushMsg("replyEnd", true);
                    clearJsonMessage();
                    tempTimer->stop();
            });
            tempTimer->start(100);
            return retJsonString;
        }
        // 检查文件是否可解析
        DocumentReader *reader = new DocumentReader(this);
        QString docText = reader->getAllText(documentPath);
        if (docText.trimmed() == "") {
            QTimer *tempTimer = new QTimer(this);
            connect(tempTimer,&QTimer::timeout,this,[=]{
                    QJsonObject jsonObj;
                    jsonObj["modelMsg"] = "ai";
                    jsonObj["colorMsg"] = "red";
                    jsonObj["isEnd"] = true;
                    QJsonDocument jDoc(jsonObj);
                    QString jsonStr = jDoc.toJson(QJsonDocument::Indented);
                    QString kbInitErrorMsg = tr("File parsing failed, please reselect the file and try again.");
                    pushMsg("updateMsg", jsonStr, kbInitErrorMsg);
                    pushMsg("replyEnd", true);
                    clearJsonMessage();
                    tempTimer->stop();
            });
            tempTimer->start(100);
            return retJsonString;
        }
        m_chat->sendMessage(msg, documentPath, m_web_search, m_enable_thinking);
    } else {
        m_chatType = NORMAL;

        QString finalMsg = msg;
        // 系统指南的UUID
        QString systemGuideUuid = "550e8400-e29b-41d4-a716-446655440002";
        // 待办生成的UUID
        QString generateTodoUuid = "550e8400-e29b-41d4-a716-446655440001";
        // 整理周报的UUID
        QString weeklyReportUuid = "550e8400-e29b-41d4-a716-446655440006";

        // 如果使用新的指令系统（commandUuid不为空）
        if (!commandUuid.isEmpty()) {
            // 判断是否是系统指南
            if (commandUuid == systemGuideUuid) {
#ifdef KB_SUPPORT_ENABLED
                // 系统指南使用知识库SDK
                if(m_KBChat != nullptr) {
                    m_KBChat->sendSystemGuideQuestion(msg);
                }
                m_chatType = KNOWLEDGEBASE; // 设置为知识库类型，以便使用知识库的回调处理
#else
                // 系统指南不支持
                m_chatType = NORMAL;
#endif
                KyInfo() << "Using new command system (System Guide) with uuid:" << commandUuid;
            } else if (commandUuid == generateTodoUuid || commandUuid == weeklyReportUuid) {
                // 待办生成/整理周报指令：将上传文件内容与输入框文本一起作为输入
                QString mergedText = msg;

                // 如果有上传的文件，读取文件中的文本内容并与输入框内容合并
                if (m_documents.count() > 0) {
                    DocumentReader *reader = new DocumentReader(this);
                    QStringList docContents;
                    for (const auto &doc : m_documents) {
                        QString path = doc.documentPath;
                        // 检查文件是否存在
                        QFileInfo fileInfo(path);
                        if (!fileInfo.exists()){
                            QTimer *tempTimer = new QTimer(this);
                            connect(tempTimer,&QTimer::timeout,this,[=]{
                                    QJsonObject jsonObj;
                                    jsonObj["modelMsg"] = "ai";
                                    jsonObj["colorMsg"] = "red";
                                    jsonObj["isEnd"] = true;
                                    QJsonDocument jDoc(jsonObj);
                                    QString jsonStr = jDoc.toJson(QJsonDocument::Indented);
                                    QString tempErrorMsg = tr("There are deleted files in the selected file. Please select again and try again.");
                                    pushMsg("updateMsg", jsonStr, tempErrorMsg);
                                    pushMsg("replyEnd", true);
                                    clearJsonMessage();
                                    tempTimer->stop();
                            });
                            tempTimer->start(100);
                            return retJsonString;
                        }
                        if (!path.isEmpty()) {
                            QString content = reader->getAllText(path);
                            if (content.trimmed() == "") {
                                QTimer *tempTimer = new QTimer(this);
                                connect(tempTimer,&QTimer::timeout,this,[=]{
                                        QJsonObject jsonObj;
                                        jsonObj["modelMsg"] = "ai";
                                        jsonObj["colorMsg"] = "red";
                                        jsonObj["isEnd"] = true;
                                        QJsonDocument jDoc(jsonObj);
                                        QString jsonStr = jDoc.toJson(QJsonDocument::Indented);
                                        QString kbInitErrorMsg = tr("File parsing failed, please reselect the file and try again.");
                                        pushMsg("updateMsg", jsonStr, kbInitErrorMsg);
                                        pushMsg("replyEnd", true);
                                        clearJsonMessage();
                                        tempTimer->stop();
                                });
                                tempTimer->start(100);
                                return retJsonString;
                            }
                            docContents.append(content);
                        }
                    }

                    if (!docContents.isEmpty()) {
                        QString allDocText = docContents.join("\n\n");
                        // 将文件内容放在用户输入前面，一并发给模型
                        if (!mergedText.isEmpty()) {
                            mergedText = allDocText + "\n\n" + mergedText;
                        } else {
                            mergedText = allDocText;
                        }
                    }
                }

                // 待办生成/整理周报指令使用 systemchat 接口，从存储文件读取提示词
                CommandInstructionModel *commandModel = CommandInstructionModel::getInstance();
                QVariantMap commandInfo = commandModel->getCommand(commandUuid);
                QString commandPrompt;

                if (!commandInfo.isEmpty()) {
                    commandPrompt = commandInfo["description"].toString();
                }

                QString currentDateStr = QDate::currentDate().toString("yyyy年MM月dd日，dddd");

                if (!commandPrompt.isEmpty()) {
                    commandPrompt.replace("{curDate}", currentDateStr);
                    finalMsg = commandPrompt + "\n当前日期：" + currentDateStr + "\n\n" + mergedText;
                } else {
                    // 如果读取失败，使用默认提示词
                    if (commandUuid == generateTodoUuid) {
                        finalMsg = QString("你是一个待办事项生成助理，你的任务是理解并分析用户输入的工作内容，然后基于这些工作内容生成待办事项。\n当前日期：") + currentDateStr + "\n\n" + mergedText;
                    } else {
                        finalMsg = QString("你是一个周报生成助手，你的任务是理解并分析用户输入的文本内容，按照要求生成一份周报。\n当前日期：") + currentDateStr + "\n\n" + mergedText;
                    }
                }

                KyInfo() << "Using new command system (Generate Todo/Weekly Report) with uuid:" << commandUuid;
                // 使用 systemchat 接口发送消息
                m_chat->sendMessage(finalMsg, "", m_web_search, m_enable_thinking, false);
            } else {
                // 其他指令获取指令描述并添加到消息前面
                CommandInstructionModel *commandModel = CommandInstructionModel::getInstance();
                QVariantMap commandInfo = commandModel->getCommand(commandUuid);
                if (!commandInfo.isEmpty()) {
                    QString commandDescription = commandInfo["description"].toString();
                    if (!commandDescription.isEmpty()) {
                        finalMsg = commandDescription + "\n\n" + msg;
                        KyInfo() << "Using new command system with uuid:" << commandUuid;
                    }
                }
                m_chat->sendMessage(finalMsg, "", m_web_search, m_enable_thinking, false);
            }
        } else if (isFromSelection) {
            // 划词时拼接划词自定义指令内容进行显示
            finalMsg = selectPrompt + msg;
            m_chat->sendMessage(finalMsg, "", m_web_search, m_enable_thinking, false);
        } else {
            m_chat->sendMessage(finalMsg, "", m_web_search, m_enable_thinking);
        }

        int sessionId = m_chat->getSession();
        if (sessionId > 0) {
            Q_EMIT sessionReplyStatusChanged(QString::number(sessionId), true, false);
        }
    }

    return retJsonString;
}

void CMsgPane::onStopReply()
{
    // 标记用户主动停止
    m_isUserStopped = true;

    // 如果追问请求正在进行，停止追问并隐藏UI
    if (m_followUpManager && m_followUpManager->isProcessing())
    {
        KyInfo() << "User stopped reply, stopping follow up request";
        m_followUpManager->stopFollowUpRequest();
    }
    
    if (m_chatType == NORMAL)
    {
        KyInfo() << "User stop chat!";
        m_chat->stopChat();
    }
    else if (m_chatType == TASK_MODE)
    {
        KyInfo() << "User stop task mode!";
        QString activeUuid = m_taskSessionManager->getActiveSessionUuid();
        if (!activeUuid.isEmpty()) {
            m_taskSessionManager->cancelResponse(activeUuid);
        }

        pushMsg("replyEnd", true);
        clearBackendRecvCache(activeUuid);
        new_reply = true;

        // task mode stop: use historyId for sessionReplyStatusChanged
        TaskSession* stopSession = m_taskSessionManager->getSession(activeUuid);
        if (stopSession) {
            Q_EMIT sessionReplyStatusChanged(QString::number(stopSession->historyId), false, false);
        }
    }
    else
    {
#ifdef KB_SUPPORT_ENABLED
        if(m_KBChat != nullptr)
        {
            if(m_KBChat->isSystemGuideMode())
            {
                KyInfo() << "User stop SystemGuide!";
                m_KBChat->stopSystemGuide();
            }
            else
            {
                KyInfo() << "User stop KBchat!";
                m_KBChat->stop();
            }
        }  // 检查是否是系统指南模式，系统指南使用独立的会话，需要单独停止

#endif
    }

    return;
}

void CMsgPane::onPrepareForShutdown(bool start)
{
    // 关机不可恢复：执行完整销毁（含删除后端会话）
    if (start) {
        onCloseAllSession(true);
    }
}

void CMsgPane::onPrepareForSleep(bool start)
{
    // 休眠可恢复：start=true 进入休眠时仅暂停、保留会话与网关上下文，不删除后端会话；
    // 唤醒(start=false)后用户在原对话继续发消息即可凭同一 session_id 续接上下文。
    if (start) {
        onCloseAllSession(false);
    }
}

void CMsgPane::onCloseAllSession(bool destroyBackend)
{
    KyInfo() << "System preparing for sleep/shutdown, stopping all activities... destroyBackend=" << destroyBackend;

    // 先处理当前活跃会话（快问快答/知识库/智能体活跃会话）
    if (m_isReplying) {
        KyInfo() << "Stopping current reply...";
        onStopReply();
        m_isUserStopped = false;
    }

    // 再处理智能体后台会话：保存缓存消息并关闭所有会话
    if (m_taskSessionManager) {
        QList<TaskSession> sessions = m_taskSessionManager->getAllSessions();
        QString activeUuid = m_taskSessionManager->getActiveSessionUuid();
        for (const TaskSession& session : sessions) {
            if (session.isReplying && session.sessionUuid != activeUuid) {
                KyInfo() << "Saving and stopping background task session for sleep/shutdown:" << session.sessionUuid;
                m_taskSessionManager->cancelResponse(session.sessionUuid);
                if (m_backendRecvMessage.contains(session.sessionUuid)
                    && !m_backendRecvMessage[session.sessionUuid].isEmpty()) {
                    QString feedbackURL;
                    if (m_backendRecvMessageHeader.contains(session.sessionUuid)) {
                        QJsonDocument headerDoc = QJsonDocument::fromJson(m_backendRecvMessageHeader[session.sessionUuid].toUtf8());
                        if (headerDoc.isObject()) {
                            feedbackURL = headerDoc.object().value("feedbackURL").toString();
                        }
                    }
                    InnerMessage shutdownMsg("", m_backendRecvReasoningMsg.value(session.sessionUuid, ""),
                                             "", false, true, 0, 0, "", feedbackURL);
                    saveBackendMessageToDatabase(session.sessionUuid, m_backendRecvMessage[session.sessionUuid], shutdownMsg);
                }
                clearBackendRecvCache(session.sessionUuid);
                Q_EMIT sessionReplyStatusChanged(QString::number(session.historyId), false, false);
            }
            // 休眠场景不销毁后端会话，保留 session_id 与网关侧上下文，唤醒后可续接
            if (destroyBackend) {
                m_taskSessionManager->deleteBackendSession(session.sessionUuid);
                m_taskSessionManager->closeSession(session.sessionUuid);
            }
        }
    }

    // 3. 通知 QML 端停止语音输入和朗读
    Q_EMIT sigStopVoiceInputAndReadingForSleep();

    KyInfo() << "All sessions stopped for sleep/shutdown.";
}

void CMsgPane::updateStateWithChat()
{
    // 主动停止时也同步前端状态，避免 UI 一直处于“正在回答”或“加载中”状态
    // 1. 更新后端记录的回复状态，保证 getIsReplying() 立即返回 false
    m_isReplying = false;
    // 2. 通过统一的 replyEnd 信号链路，触发所有监听者（包括划词弹窗里等待停止完成后再发送的问题）
    //pushMsg("replyEnd", true);
    // 3. 通知 QML 侧的 Chat.qml 更新 isReplying
    Q_EMIT replyingChanged(false);

    return;
}

///
/// \brief CMsgPane::checkChatModel  用于检查NLP模型、VISION模型、意图模型（本地NLP模型）是否配置
/// \param errorCode   错误码
/// \return 如果对应模型配置了，返回true；反之，返回false
///
bool CMsgPane::checkChatModel(int errorCode)
{
    kyai::assistant::ErrorCode error = static_cast<kyai::assistant::ErrorCode>(errorCode);
    if (error == kyai::assistant::ErrorCode::NlpModelNotFound || error == kyai::assistant::ErrorCode::VisionModelNotFound || error == kyai::assistant::ErrorCode::IntentNlpModelNotFound) {
        QJsonObject errorObject;
        errorObject["modelMsg"] = "ai";
        errorObject["noModelError"] = "true";
        QJsonDocument errorDoc(errorObject);
        QString json = errorDoc.toJson(QJsonDocument::Indented);
        QString errorMsg = "";

        switch(error){
        case kyai::assistant::ErrorCode::NlpModelNotFound:
            errorMsg = tr("<span style=\"color:red\">There are currently no available large language model available.</span> <br><br> Configuration steps: <br> 1. Go to the <strong>System Settings &gt; AI Module Management &gt; Model Configuration</strong> page to add a \"cloud model\", a \"local model\" or a \"optional model\". <br> 2. If the model configuration is already completed, please confirm whether the model is enabled. <br><br> Note: Running a local model may consume significant device resources. Please ensure your device meets the performance requirements.");
            break;
        case  kyai::assistant::ErrorCode::VisionModelNotFound:
            errorMsg = tr("<span style=\"color:red\">There are currently no available vision model available.</span> <br><br> Configuration steps: <br> 1. Go to the <strong>System Settings &gt; AI Module Management &gt; Model Configuration</strong> page to add a \"cloud model\", a \"local model\" or a \"optional model\". <br> 2. If the model configuration is already completed, please confirm whether the model is enabled. <br><br> Note: Running a local model may consume significant device resources. Please ensure your device meets the performance requirements.");
            break;
        case  kyai::assistant::ErrorCode::IntentNlpModelNotFound:
            errorMsg = tr("<span style=\"color:red\">There are currently no available large language model available.</span> <br><br> Configuration steps: <br> 1. Go to the <strong>System Settings &gt; AI Module Management &gt; Model Configuration</strong> page to add a \"cloud model\", a \"local model\" or a \"optional model\". <br> 2. If the model configuration is already completed, please confirm whether the model is enabled. <br><br> Note: Running a local model may consume significant device resources. Please ensure your device meets the performance requirements.");
            break;
        }

        pushMsg("updateMsg", json, errorMsg);

        QTimer *tempTimer = new QTimer(this);
        connect(tempTimer,&QTimer::timeout,this,[&]{
            pushMsg("replyEnd", true);
        });
        tempTimer->setSingleShot(true);
        tempTimer->start(1000);

        return false;
    }


    return true;
}

QMap<QString, CMsgPane::InformationSource> CMsgPane::getInformationSources()
{
    QMap<QString, CMsgPane::InformationSource> sources;
    int informationSourceStartId = 100;

     // 2503映射表
    sources.insert("accessories-text-editor", {"文件编辑器", "用户手册 > 文本编辑器", informationSourceStartId++});
    sources.insert("atril", {"文件查看器", "用户手册 > 文档查看器", informationSourceStartId++});
    sources.insert("biometric-manager", {"生物特征管理工具", "用户手册 > 生物特征管理工具", informationSourceStartId++});
    sources.insert("box-manager", {"文件保护箱", "用户手册 > 文件保护箱", informationSourceStartId++});
    sources.insert("engrampa", {"归档管理器", "用户手册 > 归档管理器", informationSourceStartId++});
    sources.insert("filezilla", {"FTP客户端", "用户手册 > FTP客户端", informationSourceStartId++});
    sources.insert("gparted", {"分区编辑器", "用户手册 > 分区编辑器", informationSourceStartId++});
    sources.insert("hot-key", {"快捷键", "用户手册 > 快捷键", informationSourceStartId++});
    sources.insert("kolourpaint", {"画图", "用户手册 > 画图", informationSourceStartId++});
    sources.insert("ksc-defender", {"安全中心", "用户手册 > 安全中心", informationSourceStartId++});
    sources.insert("kylin-activation", {"系统激活", "用户手册 > 系统激活", informationSourceStartId++});
    sources.insert("kylin-aiassistant", {"小K", "用户手册 > 小K", informationSourceStartId++});
    sources.insert("kylin-alarm-clock", {"闹钟", "用户手册 > 闹钟", informationSourceStartId++});
    sources.insert("kylin-burner", {"刻 录", "用户手册 > 刻 录", informationSourceStartId++});
    sources.insert("kylin-calculator", {"计算器", "用户手册 > 计算器", informationSourceStartId++});
    sources.insert("kylin-camera", {"摄像头", "用户手册 > 摄像头", informationSourceStartId++});
    sources.insert("kylin-connectivity", {"多端协同", "用户手册 > 多端协同", informationSourceStartId++});
    sources.insert("kylin-file-crush", {"文件粉碎机", "用户手册 > 文件粉碎机", informationSourceStartId++});
    sources.insert("kylin-font-viewer", {"字体管理器", "用户手册 > 字体管理器", informationSourceStartId++});
    sources.insert("kylin-installer", {"安装器", "用户手册 > 安装器", informationSourceStartId++});
    sources.insert("kylin-ipmsg", {"传书", "用户手册 > 传书", informationSourceStartId++});
    sources.insert("kylin-log-viewer", {"日志查看器", "用户手册 > 日志查看器", informationSourceStartId++});
    sources.insert("kylin-music", {"音乐", "用户手册 > 音乐", informationSourceStartId++});
    sources.insert("kylin-notebook", {"便签贴", "用户手册 > 便签贴", informationSourceStartId++});
    sources.insert("kylin-os-installer", {"系统安装", "用户手册 > 系统安装", informationSourceStartId++});
    sources.insert("kylin-os-manager", {"麒麟管家", "用户手册 > 麒麟管家", informationSourceStartId++});
    sources.insert("kylin-photo-viewer", {"看图", "用户手册 > 看图", informationSourceStartId++});
    sources.insert("kylin-printer", {"打印机", "用户手册 > 打印机", informationSourceStartId++});
    sources.insert("kylin-recorder", {"录 音", "用户手册 > 录 音", informationSourceStartId++});
    sources.insert("kylin-scanner", {"扫 描", "用户手册 > 扫 描", informationSourceStartId++});
    sources.insert("kylin-screenshot", {"截 图", "用户手册 > 截 图", informationSourceStartId++});
    sources.insert("kylin-software-center", {"软件商店", "用户手册 > 软件商店", informationSourceStartId++});
    sources.insert("kylin-usb-creator", {"U盘启动器", "用户手册 > U盘启动器", informationSourceStartId++});
    sources.insert("kylin-user-desktop", {"桌面环境", "用户手册 > 桌面环境", informationSourceStartId++});
    sources.insert("kylin-video", {"影音", "用户手册 > 影音", informationSourceStartId++});
    sources.insert("kylin-weather", {"天气", "用户手册 > 天气", informationSourceStartId++});
    sources.insert("onboard", {"屏幕键盘", "用户手册 > 屏幕键盘", informationSourceStartId++});
    sources.insert("org.remmina.Remmina", {"远程桌面客户端", "用户手册 > 远程桌面客户端", informationSourceStartId++});
    sources.insert("kylin-wacom", {"触控板", "用户手册 > 触控板", informationSourceStartId++});
    sources.insert("system-file-manager", {"文件管理器", "用户手册 > 文件管理器", informationSourceStartId++});
    sources.insert("time-shutdown", {"定时关机", "用户手册 > 定时关机", informationSourceStartId++});
    sources.insert("transmission", {"BT下载工具", "用户手册 > BT下载工具", informationSourceStartId++});
    sources.insert("ukui-control-center", {"设置", "用户手册 > 设置", informationSourceStartId++});
    sources.insert("ukui-system-monitor", {"系统监视器", "用户手册 > 系统监视器", informationSourceStartId++});
    sources.insert("utilities-terminal", {"终 端", "用户手册 > 终 端", informationSourceStartId++});
    sources.insert("yhkylin-backup-tools", {"备份还原", "用户手册 > 备份还原", informationSourceStartId++});

    return sources;
}

QString CMsgPane::getAppNameFromSourceId(int id) const
{
    QMap<QString, InformationSource> sources = getInformationSources();
    for (auto it = sources.constBegin(); it != sources.constEnd(); ++it) {
        if (it.value().id == id) {
            KyInfo() << "getAppNameFromSourceId:" << id << it.key();
            return it.key();
        }
    }
    return QString();
}

CMsgPane::InformationSource CMsgPane::getInformationSourceFromUserGuidePath(const QString &path)
{
    // 0. 检查当前是否为FAQ问答
    if(path == "用户手册->FAQ问答") {
        return {"", "用户手册->FAQ问答", -2};
    }

    // 1. 检查路径前缀并记录匹配的前缀
    QString matchedPrefix;
    if (path.startsWith(userGuidePathPrefix)) {
        matchedPrefix = userGuidePathPrefix;
    } else if (path.startsWith(kaimingUserGuidePathPrefix)) {
        matchedPrefix = kaimingUserGuidePathPrefix;
    } else {
        return {"", "", -1}; // 两个前缀都不匹配
    }

    // 2. 检查路径前缀是否匹配
    if (path.length() <= matchedPrefix.length() ) {
        return {"", "", -1}; // 无效路径返回空对象
    }

    // 3. 截取前缀后的路径，提取appName
    QString tempPath = path.mid(matchedPrefix.length());
    int pos = tempPath.indexOf("/");
    if (pos == -1) {
        return {"", "", -1};
    }
    QString appName = tempPath.left(pos);

    // 4. 从映射表中查找对应的信息
    QMap<QString, InformationSource> sources = getInformationSources();
    if (sources.contains(appName)) {
        return sources[appName];
    }

    return {"", "", -1}; // 无匹配项返回空对象
}

void CMsgPane::onRecvMsg(const InnerMessage message)
{
    QString msg = message.msg;

    KyInfo() << "onRecvMsg" << msg << message.errorCode << message.is_end << message.index << message.reasoningContentMsg << message.model << message.is_reasoning_end << message.type << message.feedbackURL;

    //NLP错误码
    if(message.errorCode != 0)
    {
        //如果上一条消息正在显示，则调用停止回答接口
        if(recv_message != "")
        {
            recv_message = "";
            recv_reasoning_message = "";

        }

        m_chatType = NORMAL;

        const bool gatewayError = (message.model == QLatin1String("Gateway"));

        //模型未配置，报错后结束会话。（网关错误保持原文，不参与 SDK 映射与「未配置模型」分支）
        if (!gatewayError && !checkChatModel(message.errorCode)) {
            return;
        }

        if (!gatewayError
            && static_cast<kyai::assistant::ErrorCode>(message.errorCode) != kyai::assistant::ErrorCode::UnknownError) {
            //对于未知错误，展示error_message字段对应的所有值   10000错误码显示sdk返回的报错信息
            msg = Globals::getErrorMsg(message.errorCode);
        }

        // 文本的网络错误和文生图的网络错误需要区分
        if (!gatewayError && message.type == "image_generation"
            && static_cast<kyai::assistant::ErrorCode>(message.errorCode) == kyai::assistant::ErrorCode::NetError) {
            msg = QObject::tr("The network connection is not available, please check the network settings and try again.");
        }

        if (gatewayError) {
            errorMessageToJs("", "ai", "red", msg);
        } else if (static_cast<kyai::assistant::ErrorCode>(message.errorCode) == kyai::assistant::ErrorCode::FreeAllocatedQuotaExceeded) {
            // 拼接红色异常提示和正常黑色提示文字
            msg = "<span style=\"color:red\">" + msg + "</span><br>" + tr("To ensure the normal use of Qi Ling, please go to System Settings > AI Module Management > Model Configuration to configure the model.");
            errorMessageToJsNoModel("", "ai", "black", msg);
        } else if (static_cast<kyai::assistant::ErrorCode>(message.errorCode) == kyai::assistant::ErrorCode::LocalNlpModelNotLoaded) {
            // 拼接红色异常提示和正常黑色提示文字
            msg = "<span style=\"color:red\">" + msg + "</span>"+ "\n\n" + tr("Configuration steps:\n1. Go to the **System Settings > AI Module Management > Model Configuration** page to add a \"cloud model\", a \"local model\" or a \"optional model\".\n2. If the model configuration is already completed, please confirm whether the model is enabled.\n\nNote: Running a local model may consume significant device resources. Please ensure your device meets the performance requirements.");
            errorMessageToJsNoModel("", "ai", "black", msg);
        } else {
            errorMessageToJs("", "ai", "red", msg);
        }

        clearJsonMessage();
        return;
    }

    jsonObjToWeb["modelMsg"] = message.model;
    jsonObjToWeb["feedbackURL"] = message.feedbackURL;

    QJsonDocument aiJsonSendWebDoc(jsonObjToWeb);
    QString jsonMsgHeadToWeb = aiJsonSendWebDoc.toJson(QJsonDocument::Indented);
    // 如果是新消息的第一个回答，设置消息头，清空文本，准备显示正文
    if(new_reply){
        pushMsg("updateMsg", jsonMsgHeadToWeb, "");
        clearJsonMessage();
    }
    recv_message.append(msg);
    recv_reasoning_message.append(message.reasoningContentMsg);
    // 替换思考过程中的<>转义一下，防止思考过程显示异常
    recv_reasoning_message.replace("<","&lt;");
    recv_reasoning_message.replace(">","&gt;");

    recv_message.replace("<","&lt;");
    recv_message.replace(">","&gt;");

    new_reply = message.is_end;
    m_is_reasoning_end = message.is_reasoning_end;

    if (message.is_end) {
        QString sessionId = QString::number(m_chat->getSession());
        Q_EMIT sessionReplyStatusChanged(sessionId, false, false);
    }

    updateBubble();
}

void CMsgPane::onRecvTaskMsg(const QString& sessionUuid, const InnerMessage message)
{
    QString msg = message.msg;

    // 助手端过滤掉部分已知不能渲染的符号
    msg = msg.replace("🔥","");
    msg = msg.replace("🏢","");
    msg = msg.replace("🤖","");

    KyInfo() << "onRecvTaskMsg msg===" << msg << message.type;

    TaskSession* session = m_taskSessionManager->getSession(sessionUuid);
    if (!session) {
        KyInfo() << "Received message for unknown session, ignoring:" << sessionUuid;
        return;
    }

    if (session->isCancelled) {
        if (!message.is_end) {
            KyInfo() << "Discarding message for cancelled session:" << sessionUuid;
            return;
        }
        session->isCancelled = false;
        KyInfo() << "Received end message for cancelled session, resetting:" << sessionUuid;
        return;
    }

    session->isReplying = !message.is_end;

    if (message.type == "tool_call" || message.type == "tool_result") {
        QJsonDocument toolDoc = QJsonDocument::fromJson(message.extraJson.toUtf8());
        KyInfo() << "toolDoc.object()==" << toolDoc.object();
        if (toolDoc.isObject()) {
            m_backendTaskToolCalls[sessionUuid].append(toolDoc.object());
        }
        if (sessionUuid == m_taskSessionManager->getActiveSessionUuid()) {
            if (new_reply) {
                jsonObjToWeb["modelMsg"] = modelMsgForTaskMode();
                QJsonDocument aiJsonSendWebDoc(jsonObjToWeb);
                QString jsonMsgHeadToWeb = aiJsonSendWebDoc.toJson(QJsonDocument::Indented);
                m_backendRecvMessageHeader[sessionUuid] = jsonMsgHeadToWeb;
                pushMsg("updateMsg", jsonMsgHeadToWeb, "");
                clearJsonMessage();
            }
            Q_EMIT updateTaskToolCalls(message.extraJson);
        }
        return;
    }

    if (sessionUuid == m_taskSessionManager->getActiveSessionUuid()) {
        if (message.errorCode != 0) {
            clearBackendRecvCache(sessionUuid);

            m_chatType = TASK_MODE;

            const bool gatewayNoModel = message.model == QLatin1String("Gateway")
                && message.errorCode == kGatewayInnerErrorNoModelConfigured;
            if (gatewayNoModel) {
                const QString friendly = tr("No available AI model detected. AI tasks cannot start. Please complete configuration in model management.");
                errorMessageToJsNoModel("", "ai", "red", friendly);
                clearJsonMessage();
                TaskSession* errorSession = m_taskSessionManager->getSession(sessionUuid);
                if (errorSession) {
                    Q_EMIT sessionReplyStatusChanged(QString::number(errorSession->historyId), false, false);
                }
                return;
            }

            if (message.model != QLatin1String("Gateway")) {
                msg = Globals::getErrorMsg(message.errorCode);
            }

            errorMessageToJs("", "ai", "red", msg);
            clearJsonMessage();
            TaskSession* errorSession = m_taskSessionManager->getSession(sessionUuid);
            if (errorSession) {
                Q_EMIT sessionReplyStatusChanged(QString::number(errorSession->historyId), false, false);
            }
            return;
        }

        jsonObjToWeb["modelMsg"] = modelMsgForTaskMode();
        jsonObjToWeb["feedbackURL"] = message.feedbackURL;

        QJsonDocument aiJsonSendWebDoc(jsonObjToWeb);
        QString jsonMsgHeadToWeb = aiJsonSendWebDoc.toJson(QJsonDocument::Indented);

        if (new_reply) {
            m_backendRecvMessageHeader[sessionUuid] = jsonMsgHeadToWeb;
            pushMsg("updateMsg", jsonMsgHeadToWeb, "");
            clearJsonMessage();
        }

        m_backendRecvMessage[sessionUuid].append(msg);
        m_backendRecvReasoningMsg[sessionUuid].append(message.reasoningContentMsg);
        QString escapedReasoningMsg = m_backendRecvReasoningMsg[sessionUuid];
        escapedReasoningMsg.replace("<", "&lt;");
        escapedReasoningMsg.replace(">", "&gt;");

        new_reply = message.is_end;
        m_is_reasoning_end = message.is_reasoning_end;

        // 非结束消息，更新思考过程和正文
        if (!message.is_end) {
            if (message.reasoningContentMsg != "") {
            pushMsg("updateReasoningMsg", "", escapedReasoningMsg);
            }
            if (msg != "") {
                pushMsg("updateMsg", "", m_backendRecvMessage[sessionUuid]);
            }
        } else {
            // Ollama 等模型常常不发送流式 chunk，只在结束时返回一条 done 帧
            QString finalMsg = msg;
            if (finalMsg.isEmpty()) {
                finalMsg = m_backendRecvMessage.value(sessionUuid);
            } else {
                m_backendRecvMessage[sessionUuid] = finalMsg;
            }
            if (finalMsg != "") {
                pushMsg("updateMsg", "", finalMsg);
            } else {
                QJsonObject modelHeader;
                modelHeader["modelMsg"] = modelMsgForTaskMode();
                pushMsg("updateMsg", QJsonDocument(modelHeader).toJson(QJsonDocument::Compact), "");
            }
            pushMsg("replyEnd", true);
            clearBackendRecvCache(sessionUuid);
            TaskSession* endSession = m_taskSessionManager->getSession(sessionUuid);
            if (endSession) {
                Q_EMIT sessionReplyStatusChanged(QString::number(endSession->historyId), false, false);
            }
        }
    } else {
        if (!m_backendRecvMessage.contains(sessionUuid)) {
            QJsonObject header;
            header["modelMsg"] = modelMsgForTaskMode();
            header["feedbackURL"] = message.feedbackURL;
            m_backendRecvMessageHeader[sessionUuid] = QJsonDocument(header).toJson(QJsonDocument::Compact);
        }
        // 由于最后一条消息会返回所有消息内容，所以最后一条消息不保存
        if (!message.is_end) {
            m_backendRecvMessage[sessionUuid].append(msg);
            m_backendRecvReasoningMsg[sessionUuid].append(message.reasoningContentMsg);
        } else {
            {
                QString notifyMsg = msg;
                notifyMsg.replace(QRegularExpression("```[\\s\\S]*?```"), "");
                notifyMsg.replace(QRegularExpression("`([^`]+)`"), "\\1");
                notifyMsg.replace(QRegularExpression("!\\[([^\\]]*)\\]\\([^\\)]*\\)"), "\\1");
                notifyMsg.replace(QRegularExpression("\\[([^\\]]+)\\]\\([^\\)]*\\)"), "\\1");
                notifyMsg.replace(QRegularExpression("(?m)^\\s{0,3}(#{1,6})\\s+"), "");
                notifyMsg.replace(QRegularExpression("(?m)^\\s{0,3}[-*_]{3,}\\s*$"), "");
                notifyMsg.replace(QRegularExpression("(?m)^\\s{0,3}[-*+]\\s+"), "");
                notifyMsg.replace(QRegularExpression("(?m)^\\s*\\d+\\.\\s+"), "");
                notifyMsg.replace(QRegularExpression("\\*\\*([^*]+)\\*\\*"), "\\1");
                notifyMsg.replace(QRegularExpression("__([^_]+)__"), "\\1");
                notifyMsg.replace(QRegularExpression("\\*([^*]+)\\*"), "\\1");
                notifyMsg.replace(QRegularExpression("_([^_]+)_"), "\\1");
                notifyMsg.replace(QRegularExpression("~~([^~]+)~~"), "\\1");
                notifyMsg.replace(QRegularExpression("[\\r\\n]+"), " ");
                notifyMsg = notifyMsg.simplified();
                if (notifyMsg.length() > 100){
                    notifyMsg = notifyMsg.left(100);
                }
                KyInfo() << "notifyMsg=====" << notifyMsg;
                NotificationNotify::getInstance()->sendNotification(QObject::tr("Agent"), notifyMsg, QString::number(session->historyId));
            }
            QString finalMsg = msg.isEmpty() ? m_backendRecvMessage.value(sessionUuid) : msg;
            const QString historyIdStr = QString::number(session->historyId);
            saveBackendMessageToDatabase(sessionUuid, finalMsg, message);
            clearBackendRecvCache(sessionUuid);
            m_database->setUnreadFlag(historyIdStr, 1);
            Q_EMIT sessionReplyStatusChanged(historyIdStr, false, true);
            Q_EMIT scheduledSessionCreated(historyIdStr);
            if (sessionUuid != m_taskSessionManager->getActiveSessionUuid()) {
                m_taskSessionManager->closeSession(sessionUuid);
            }
        }
    }
}

void CMsgPane::clearBackendRecvCache(const QString& sessionUuid)
{
    m_backendRecvMessage.remove(sessionUuid);
    m_backendRecvMessageHeader.remove(sessionUuid);
    m_backendRecvReasoningMsg.remove(sessionUuid);
    m_backendTaskToolCalls.remove(sessionUuid);
}

void CMsgPane::saveBackendMessageToDatabase(const QString& sessionUuid, const QString& message, const InnerMessage& msgInfo)
{
    QJsonObject json;
    json["author"] = "Bot";
    json["isEnd"] = true;
    json["isRefreshButtonVisiable"] = true;
    json["message"] = message;
    json["modelMsg"] = modelMsgForTaskMode();
    json["reasonMessage"] = msgInfo.reasoningContentMsg;
    json["feedbackURL"] = msgInfo.feedbackURL;
    json["colorMsg"] = "";
    json["drawMsg"] = false;
    json["imageSrc"] = "";
    json["imagePath"] = "";
    json["showImageOptions"] = false;
    json["showRead"] = true;
    json["showRead"] = true;
    json["noModelError"] = "";
    json["pdfPath"] = "";
    if (m_backendTaskToolCalls.contains(sessionUuid) && !m_backendTaskToolCalls[sessionUuid].isEmpty()) {
        QJsonObject toolCallsObj;
        toolCallsObj["toolCalls"] = m_backendTaskToolCalls[sessionUuid];
        json["taskToolCalls"] = QString::fromUtf8(QJsonDocument(toolCallsObj).toJson(QJsonDocument::Compact));
    }

    QString jsonStr = QJsonDocument(json).toJson(QJsonDocument::Compact);

    TaskSession* session = m_taskSessionManager->getSession(sessionUuid);
    QString historyIdStr = session ? QString::number(session->historyId)
                                   : m_database->selectSessionIDBySessionUuid(sessionUuid);
    if (historyIdStr.isEmpty()) {
        KyInfo() << "saveBackendMessageToDatabase: missing historyId for sessionUuid=" << sessionUuid;
        return;
    }

    int msgIndex = m_database->selectBySessionID(historyIdStr).size();

    m_database->insertOneWithModeTypeAndUuid(historyIdStr, msgIndex, jsonStr, 0, 1, sessionUuid);
    m_database->updateOperateTime(historyIdStr, msgIndex);
}

void CMsgPane::onAddQuestion(const QStringList &questions)
{
    Q_EMIT addQuestion(questions);
}

void CMsgPane::onAudioRecvMsg(QString msg, bool is_end, int index, int errorCode, QString model)
{
    if(errorCode != 0)
    {
        //如果上一条消息正在显示，则停止显示动画，输出全部文字
        if(recv_message != "")
        {
            onStopReply();
        }

        //模型未配置的错误需要单独处理
        if (errorCode == AISDK_MODEL_NOT_FOUND){

            //启用计时器，当前端和后端都收到停止信号后，发送错误消息
            QTimer *tempTimer = new QTimer(this);
            connect(tempTimer,&QTimer::timeout,this,[=]{
                if (getIsEnd()){

                    QJsonObject jsonObj;
                    jsonObj["isFirst"] = true;
                    jsonObj["modelMsg"] = "ai";
                    jsonObj["noModelError"] = "true";
                    jsonObj["voiceModelError"] = true;
                    QJsonDocument jDoc(jsonObj);
                    QString jsonStr = jDoc.toJson(QJsonDocument::Indented);
                    QString errorStr = tr("<span style=\"color:red\">There are currently no available voice models available.</span> <br><br> Configuration steps: <br> 1. Go to the <strong>System Settings &gt; AI Module Management &gt; Model Configuration</strong> page to add a \"cloud model\", a \"local model\" or a \"optional model\". <br> 2. If the model configuration is already completed, please confirm whether the model is enabled. <br><br> Note: Running a local model may consume significant device resources. Please ensure your device meets the performance requirements.");
                    pushMsg("addRecvMsg", jsonStr, errorStr);
                    pushMsg("replyEnd", true);

                    tempTimer->stop();
                }

            });
            tempTimer->start(100);
//TODO：需要基于新版本的音频管理控件实现取消语音输入功能
//            Q_EMIT cancelSendAudio();
//            Q_EMIT sigVoiceInputError(Globals::getErrorMsg(static_cast<SpeechErrorCode>(errorCode)), errorCode);

            return;
        }
        QTimer *tempTimer2 = new QTimer(this);
        connect(tempTimer2,&QTimer::timeout,this,[=]{
            if (getIsEnd()){
                QJsonObject jsonObj;
                jsonObj["isFirst"] = true;
                QJsonDocument jDoc(jsonObj);
                QString jsonStr = jDoc.toJson(QJsonDocument::Indented);
                pushMsg("addRecvMsg", jsonStr, "");
                errorMessageToJs("", "ai", "red", msg);
                clearJsonMessage();

                tempTimer2->stop();
            }

        });
        tempTimer2->start(100);
//TODO：需要基于新版本的音频管理控件实现取消语音输入功能
//        // 发出语音输入错误信号，供写作等模块使用
//        Q_EMIT sigVoiceInputError(msg, errorCode);

        return;
    }
}

void CMsgPane::onRecvTool(QString msg, QMap<int, QString> imageToolMap, QString imagePath, int errorCode, QString model, QString feedbackURL)
{
//    if(is_thinking == true)
//    {
//        is_thinking = false;
//    }

    //NLP错误码
    if(errorCode != 0)
    {
        errorMessageToJs("", "ai", "red", msg);
        //pushMsg("replyEnd", true);
        clearJsonMessage();
        return;
    }

    QJsonObject jsonObj;
    QJsonArray jsonArray;
    for(auto i = imageToolMap.constBegin(); i != imageToolMap.constEnd(); ++i)
    {
        QJsonObject toolObj;
        toolObj["tool_id"] = i.key();
        toolObj["tool_name"] = i.value();
        jsonArray.append(toolObj);
    }
    jsonObj["pictureOperates"] = jsonArray;
    QJsonDocument doc(jsonObj);
    QString jsonData = doc.toJson(QJsonDocument::Indented);

    QJsonObject todoJsonObjToWeb;
    todoJsonObjToWeb["msgHead"] = msgHead;
    todoJsonObjToWeb["promptId"] = promptId;
    todoJsonObjToWeb["promptIconPath"] = promptIconPath;
    todoJsonObjToWeb["msg"] = msg;
    todoJsonObjToWeb["imagePath"] = imagePath;
    todoJsonObjToWeb["modelMsg"] = model;
    todoJsonObjToWeb["feedbackURL"] = feedbackURL;
    QJsonDocument aiJsonSendWebDoc(todoJsonObjToWeb);
    QString scheJsonMsgHeadToWeb = aiJsonSendWebDoc.toJson(QJsonDocument::Indented);

    pushMsg("updateMsg", scheJsonMsgHeadToWeb, msg);

    pushMsg("appendRecvTool", jsonData, msg);
    Q_EMIT updateOperateRefresh();//图像处理的重新回答不可用
    pushMsg("replyEnd", true);
}

void CMsgPane::onGenerateSummary(QString msg, bool is_end, int index, int errorCode, QString model, const QString &reasoningContentMsg)
{
    msg.replace("\\", "\\\\");
    msg.replace("\n", "\\n");
    msg.replace("\"", "\\\"");
    m_summary.append(msg);
    //KyInfo() << "本次会议助手使用的nlp模型是<<" << model;
    //KyInfo() << "本次会议助手的智能总结深度思考内容是" <<reasoningContentMsg;
    KyInfo() << "本次会议助手总结内容<<" << m_summary;
    //深度思考不为空时需要在界面提醒用户当前深度思考需要较多时间
    if(reasoningContentMsg.isEmpty()){
        Q_EMIT sigAddSummaryTips(false);
    }
    else {
        Q_EMIT sigAddSummaryTips(true);
    }
    if(is_end)
    {
        saveMeetingToFile(model);
        // 会议助手功能已移除
        m_summary = "";
    }
}
void CMsgPane::saveMeetingMsgToDataBase(int msgIndex, QString message)
{
    if(msgIndex == -1)
    {
        KyInfo() << "当前会议助手消息索引为-1，消息无效，无需存入数据库";
        return;
    }
    Q_UNUSED(message);
    // 会议助手功能已下线，数据库不再记录会议内容
}

void CMsgPane::saveMeetingInfoToDataBase(int msgIndex, QString summary, QString fileName, QString filePath)
{
    Q_UNUSED(msgIndex);
    Q_UNUSED(summary);
    Q_UNUSED(fileName);
    Q_UNUSED(filePath);
    // 会议助手功能已下线，数据库不再记录会议元信息
}

void CMsgPane::deleteMeetingHistoryFromDataBase(const int meetingID)
{
    KyInfo() << "根据会议ID删除数据库中会议记录内容" << meetingID;
    if(meetingID != -1)
    {
        bool flag = m_database->deleteByMeetingID(meetingID);
        if(flag){
            KyInfo() << meetingID <<"会议记录保存数据库数据已删除";
        }
        else
        {
            KyInfo() << meetingID <<"会议记录保存数据库数据删除失败";
        }

    }

}

void CMsgPane::deleteMeetingHistoryFromDataBase(const QString meetingName)
{
      KyInfo() << "根据会议名称删除数据库中会议记录内容" << meetingName;
      int meetingID = m_database-> getMeetingIDFromName(meetingName);
      if(meetingID != -1)
      {
          bool flag = m_database->deleteByMeetingID(meetingID);
          if(flag){
              KyInfo() << meetingID <<"会议记录保存数据库数据已删除";
          }
          else
          {
              KyInfo() << meetingID <<"会议记录保存数据库数据删除失败";
          }

      }
      else
      {
          KyInfo() << meetingID <<"会议记录保存数据库数据删除失败";

      }
}

void CMsgPane::onMeetingRunningPrevent(bool isShutDown)
{
    //meetingInhibit();
}

void CMsgPane::clearMeetingUsingStatus()
{
    meetingUnInhibit();
}

QString CMsgPane::getActiveMeetingName()
{
    // 会议助手功能已下线，当前无激活的会议名称
    return QString();
}

void CMsgPane::onShowText(const QString text)
{
    pushMsg("showText", "", text);
}

void CMsgPane::onShowSpeech(const QString text)
{
    //    QString msg = this->m_finishedSpeech + text;
    //    msg.replace('\n', "");
    //    QString js_str = QString("updateMsg('%1')").arg(msg);
    //    KyInfo() << js_str;
    //    m_pViewChat->page()->runJavaScript(js_str);
}

void CMsgPane::onAppendSpeech(const int speakerID, const QString text)
{
    ModelConfig mc;
    std::string modelStr = mc.getCurrentModelName(AiCapability::Speech, DeployType::PublicCloud);
    QString tmpStr = QString::fromStdString(modelStr);
    jsonObjToWeb["modelMsg"] = tmpStr;
    jsonObjToWeb["speakerId"] = speakerID;
    QJsonDocument modelJsonSendWebDoc(jsonObjToWeb);
    QString jsonMsgHeadToWeb = modelJsonSendWebDoc.toJson(QJsonDocument::Indented);
    Q_EMIT pushMeetingMsg(jsonMsgHeadToWeb,text);//开始发送会议记录总结信息
    clearJsonMessage();
}

void CMsgPane::updateToShowPic(QString imageData, QString errorMessage)
{
        //如果文生图生成失败，向前端插入生成失败的图片
    if (errorMessage == "failed"){
        m_operatorId = 4;
        Q_EMIT insertImage("", m_operatorId, imagePath);
        m_isUserStopped = false;
        return;
    }

    if(imageData != ""){
        QString str = QString("data:" + imageData);
        Q_EMIT insertImage(str, m_operatorId, imagePath);
        m_isUserStopped = false;
    }else{
        Q_EMIT sigCancelGenerateImage();
        m_isUserStopped = false;
    }

    pushMsg("replyEnd", true);
}

void CMsgPane::updateToShowOperatePic(QString imageData, QString errorMessage)
{
    if(imageData != "")
    {
        QString str = QString("data:" + imageData);
        Q_EMIT insertOperateImage(str, m_operatorId, imagePath);
        Q_EMIT updateOperateRefresh();//图像处理的重新回答不可用
        Q_EMIT moveScrollBar();
    }
    pushMsg("replyEnd", true);
    clearJsonMessage();
}

/**
 * @brief CMsgPane::updateToShowAIPic  AI文生图返回图片函数
 * @param imageData 图像数据
 * @param errorMessage 错误信息
 */
//void CMsgPane::updateToShowAIPic(VisionImageData *imageData, QString errorMessage)
//{
//    KyInfo() << "updateToShowAIPic" << imageData;
//    if(imageData != nullptr)
//    {
//        KyInfo() << "图片base64 data数据：" << imageData->data << "图片字节数：" << imageData->data_size << "图像宽高：" << imageData->width << imageData->height;

//        //上传图像到html，并更新当前的对话框
//        QPixmap map;
//        map.loadFromData(imageData->data, imageData->data_size);

//        QImage image = map.toImage();
//        // 将QImage转换为QByteArray
//        QByteArray byteArray;
//        QBuffer buffer(&byteArray);
//        buffer.open(QIODevice::WriteOnly);
//        image.save(&buffer, "PNG");
//        buffer.close();
//        QString str = QString("data:image/png;base64," + byteArray.toBase64());
//        Q_EMIT insertImage(str);
//    }
//    if(!errorMessage.isEmpty())
//    {
//        KyInfo() << "图片生成失败，imageData为空，错误信息(存在多种错误反馈)： " << errorMessage;
//        jsonObjToWeb["modelMsg"] = "ai";
//        jsonObjToWeb["colorMsg"] = "red";
//        QJsonDocument aiJsonSendWebDoc(jsonObjToWeb);
//        QString jsonMsgHeadToWeb = aiJsonSendWebDoc.toJson(QJsonDocument::Indented);
//        pushMsg("updateMsg", jsonMsgHeadToWeb, errorMessage + "，请重新尝试。");
//    }
//    pushMsg("replyEnd", true);
//    clearJsonMessage();
//}

/**
 * @brief CMsgPane::updateBubble
 * 原功能：按字去更新内容
 * 现功能：接收SDK返回的文本显示到前端
 */
void CMsgPane::updateBubble()
{
    KyInfo() << "updateBubble" << recv_message;
    //显示推理过程
    if(recv_reasoning_message != ""){
        KyInfo() << "思考过程message=" << recv_reasoning_message;
        // 思考过程消息加载
        if(new_reply)
        {
            recv_message = "";
            recv_reasoning_message = "";
            pushMsg("replyEnd", true);
            m_chatType = NORMAL;
            return;
        }
        if(m_is_reasoning_end)
        {
            KyInfo() << "思考过程回答结束";
            recv_reasoning_message = "";
            return;
        }
        QString show_reason_msg = recv_reasoning_message;
        pushMsg("updateReasoningMsg", "", show_reason_msg);
    }else{
        // 总结消息加载
        if(new_reply)
        {
            //对于is_end信号和msg一起返回的消息，需要再向前端更新一次文本
            if(recv_message != ""){
                pushMsg("updateMsg", "", recv_message);
            }
            KyInfo() << "recv_message" << recv_message;
            // 只有在追问模式启用时才触发追问信息构建
            if (Globals::getinstance()->mQuestionModeEnabled) {
                buildFollowUpRequest(); //触发追问信息构建
            }
            recv_message = "";
            recv_reasoning_message = "";
            //如果上一条消息是整理周报，那么在回答结束时生成周报
            QString weeklyReportUuid = "550e8400-e29b-41d4-a716-446655440006";
            if(commandUuid == weeklyReportUuid)
            {
                Q_EMIT saveToPDF();
                return;
            }
            pushMsg("replyEnd", true);
            m_chatType = NORMAL;

            //语音模式下记录当前回答的所有内容并在回答结束后进行朗读  --该功能已删除
            return;
        }
        QString show_msg = recv_message;
        pushMsg("updateMsg", "", show_msg);
    }

    return;
}

//无参，多用于控制前端状态变化
void CMsgPane::pushMsg(QString func)
{
    if(func == "saveLastMsgToDatabase"){
        Q_EMIT saveLastMsgToDatabase();
    }
    else if(func == "stopVoiceInputIfActive"){
        Q_EMIT sigStopVoiceInputIfActive();
    }
}

//单参，多用于修改某属性的值
void CMsgPane::pushMsg(QString func, QVariant arg1)
{
    if (func == "replyEnd"){
        bool flag = arg1.toBool();
        Q_EMIT replyEnd(flag);
    }
    else if(func == "addReportPath"){
        QString path = arg1.toString();
        Q_EMIT addReportPath(path);
    }
}

//消息类发送接口，所有对话类消息通过此处发送至前端
void CMsgPane::pushMsg(QString func, QString jsonMsgHeadToWeb, QString msg)
{
    QString markdownMsg = msg;
    if (func == "addMsg") {
        Q_EMIT addMsg(jsonMsgHeadToWeb, msg);
    }

    // KyInfo() << "pre msg" << msg;

    // 若内容已被识别为「HTML」，则不再做 Markdown→HTML，避免重复包裹。
    // 注意：不能用任意「<...>」判断——文档/命令里的占位符（如 <本地用户名>）、
    // 不等式片段等会误匹配，导致跳过 convertMarkDownToHtml，界面出现「Markdown 平铺」。
    static const QRegularExpression preRenderedHtmlTagPattern(
        QStringLiteral(R"(<\s*/?(?:p|div|span|br|hr|h[1-6]|ul|ol|li|table|tr|td|th|thead|tbody|tfoot|caption|colgroup|col|a|strong|em|b|i|u|pre|code|blockquote|section|article|nav|header|footer|main|aside|dl|dt|dd|img|figure|figcaption|svg|video|audio|source|canvas|body|html|head|meta|link|style|script|title|form|input|button|label|select|option|textarea|font|center|small|sub|sup)\b)"),
        QRegularExpression::CaseInsensitiveOption);
    QRegularExpressionMatch match = preRenderedHtmlTagPattern.match(msg);
    // 仅当存在常见 HTML 标签名（ASCII）时视为已是 HTML，否则按 Markdown 转换
    if(msg != "" && !match.hasMatch()){
        msg = normalizeAtxHeadingsForDisplay(msg);
        markdownMsg = msg;
        msg = convertMarkDownToHtml(msg);
    }


    // KyInfo() << "msg" << msg;

    if (func == "addRecvMsg") {
        Q_EMIT addRecvMsg(jsonMsgHeadToWeb, msg);
    }

    if (func == "updateMsg") {
        Q_EMIT updateMsg(jsonMsgHeadToWeb, msg, markdownMsg);
    }
    if(func == "addRecvSources"){
        Q_EMIT addRecvSources(jsonMsgHeadToWeb);
    }
    if(func == "appendRecvTool"){
        Q_EMIT appendRecvTool(jsonMsgHeadToWeb);
    }
    if (func == "updateReasoningMsg") {
        Q_EMIT updateReasoningMsg(jsonMsgHeadToWeb, msg);
    }
}

bool CMsgPane::getIsEnd()
{
    bool currentIsEnd  = new_reply && !m_isReplying;
    return  currentIsEnd;
}

void CMsgPane::showTip(QString tipMessage)
{
    Q_EMIT showTipMessage(tipMessage);
}

bool CMsgPane::isCommunity()
{
    static int sIsCommunity = -1;
    if(sIsCommunity == -1)
    {
        QString filename = "/etc/os-release";
        QSettings osSettings(filename, QSettings::IniFormat);

        QString versionID = osSettings.value("VERSION_ID").toString();

        if(versionID.compare("22.04", Qt::CaseSensitive))
        {
            sIsCommunity = 0;
        }
        else
        {
            sIsCommunity = 1;
        }
    }
    return (bool)(sIsCommunity);
}

QString CMsgPane::markdownToHtml(const std::string &markdown)
{
    // Convert the markdown text to HTML
    const char* markdownText = markdown.c_str();
    char* html = cmark_markdown_to_html(markdownText, strlen(markdownText), CMARK_OPT_DEFAULT);

    // Handle the conversion result
    std::string htmlStr(html);
    //cmark_mem_free(html);
    QString qStr = QString::fromStdString(htmlStr);
    return qStr;
}

void CMsgPane::getMeetingList()
{
    //扫描本地文件生成List
    QString directoryPath = Globals::defaultPath + "/meeting";
    QDir dir(directoryPath);
    if (!dir.exists()) {
        KyInfo() << "目录不存在：" << directoryPath;
        return;
    }

    QJsonArray jsonArray;
    QStringList filters;
    filters << "*.txt" ; // 设置过滤器以只获取txt文件
    QStringList entries = dir.entryList(filters, QDir::Files); // 获取文件列表
    foreach (const QString &entry, entries) {
        QFileInfo fileInfo(dir.filePath(entry));
        QString fileName = fileInfo.fileName();
        int indexUnder = fileName.indexOf('_');
        int indexDot = fileName.indexOf('.');

        QJsonObject jsonObject;
        QString index = QString::number(entries.indexOf(entry));
        jsonObject["index"] = index;
        jsonObject["path"] = directoryPath + "/" + fileName;
        jsonObject["name"] = fileInfo.baseName();
        jsonObject["time"] = fileInfo.birthTime().toString("yyyy/MM/dd · hh:mm:ss");
        jsonObject["reloading"] = false;
        if(fileInfo.suffix() == "temp")
        {
            jsonObject["generating"] = "true";
         }
        else
        {
            jsonObject["generating"] = "false";
         }
        jsonArray.append(jsonObject);

    }

    //扫描数据库中会议助手表查看是否有未生成的会议记录总结数据提示给用户
    QJsonArray errorResultJsonArray  = m_database->selectAllErrorMeeting();
     //合并两个模块并进行转换
    for (const auto &value : errorResultJsonArray) {
           jsonArray.append(value);
       }
    // 对合并后的数组进行文件名称排序
    std::vector<QJsonValue> vec(jsonArray.begin(), jsonArray.end());
    std::sort(vec.begin(), vec.end(), []( QJsonValue a,  QJsonValue b) {
        // 将 QJsonValue 转换为 QJsonObject
        QJsonObject objA = a.toObject();
        QJsonObject objB = b.toObject();

        // 提取 "time" 字段并将其转换为 QDateTime 对象
        QString timeA = objA.value("time").toString();
        QString timeB = objB.value("time").toString();

        // 比较文件名中的时间
        return timeA > timeB;
    });

    QJsonArray lastJsonArray;
    for(int i =0;i <vec.size();i++)
    {
        lastJsonArray.append(vec.at(i));
    }
    QString allResult =  QJsonDocument(lastJsonArray).toJson(QJsonDocument::Indented);

    Q_EMIT loadMeetingList(allResult);
}

void CMsgPane::openMeetingFile(QString path)
{
    QProcess process;
    QString command = "pluma";
    QStringList arguments;
    arguments << path;
    process.startDetached(command, arguments);
    process.waitForFinished(-1);
    // 检查进程是否正常退出
    if (process.exitStatus() == QProcess::NormalExit) {
        KyInfo() << "Pluma opened the file successfully!";
    } else {
        KyInfo() << "Failed to open the file with Pluma!";
    }
}

void CMsgPane::openMeetingFolder(QString path)
{
    QProcess process;
    QString command = "peony";
    QStringList arguments;
    arguments << path;
    process.startDetached(command, arguments);
    process.waitForFinished(-1);
    // 检查进程是否正常退出
    if (process.exitStatus() == QProcess::NormalExit) {
        KyInfo() << "peony opened the dir successfully!";
    } else {
        KyInfo() << "Failed to open the dir with Pluma!";
    }
}

int CMsgPane::xdgOpen(QString path)
{
    // 1. 检查路径是否存在
    if (!QFileInfo::exists(path)) {
        KyInfo() << "xdgOpen failed: path does not exist:" << path;
        return 1;
    }

    // 2. 创建QProcess对象（栈上创建，自动释放）
    QProcess process;
    // 设置要执行的命令和参数
    process.start("xdg-open", QStringList() << path);

    // 3. 等待进程退出并获取退出码（设置超时时间，根据需求调整）
    // waitForFinished：等待进程执行完成（返回是否正常退出）
    if (!process.waitForFinished(10000)) { // 10秒超时（xdg-open通常很快退出）
        KyInfo() << "xdgOpen failed: process timeout or crashed";
        KyInfo() << "Error reason:" << process.errorString();
        process.kill(); // 超时后杀死进程，避免僵尸进程
        return -1;
    }

    // 4. 获取xdg-open的实际返回值（退出码）
    int exitCode = process.exitCode();
    if (exitCode == 0) {
        KyInfo() << "xdgOpen successfully executed! exit code:" << exitCode;
        return 0;
    } else {
        KyInfo() << "xdgOpen failed: exit code =" << exitCode;
        QByteArray stderrData = process.readAllStandardError();
        if (!stderrData.isEmpty()) {
            KyInfo() << "xdg-open error output:" << stderrData;
        }
        return exitCode;
    }

    return 0;
}

void CMsgPane::xdgOpenForImageOperate(QString base64Image, int operatorId, QString imagePath)
{
    if(base64Image == ""){
        return;
    }
    if (m_xdgOpenForImageOperateInProgress) {
        KyInfo() << "xdgOpenForImageOperate already in progress, ignore duplicate request";
        return;
    }

    QString imageFileName = ""; // 文件名
    QString savePath = QDir::homePath().append("/.config/kylin-aiassistant/operate-temp/"); // 实际保存文件路径

    // 若图片路径不为空，并且消息提为空则展示图片路径
    if(imagePath != ""){
        QFileInfo fileInfo(imagePath);
        QString fileName = fileInfo.baseName();
        if(fileName != ""){
            imageFileName = fileName;
        }else{
            imageFileName = imagePath;
        }
    }
    if(operatorId == 1){
        imageFileName = imageFileName.append(tr("-ScannedBlackWhiteDocument.png"));
    }else if(operatorId == 2){
        imageFileName = imageFileName.append(tr("-SmartCutouts.png"));
    }else{
//        imageFileName = "operate-temp.png";
        QDateTime currentTime = QDateTime::currentDateTime();
        QString timestamp = currentTime.toString("yyyyMMdd_hhmmss"); // 格式: 年月日_时分秒
        imageFileName = QString("aiassistant_%1_temp.png").arg(timestamp);
    }
    savePath = savePath.append(imageFileName);

    QString operate_path = QDir::homePath().append("/.config/kylin-aiassistant/operate-temp");
    if (!QDir().exists(operate_path))
        QDir().mkdir(operate_path);

    m_xdgOpenForImageOperateInProgress = true;

    QString savePathCopy = savePath;

    // base64 解码和落盘可能耗时较长，放到后台线程，避免阻塞 UI。
    auto *watcher = new QFutureWatcher<bool>(this);
    connect(watcher, &QFutureWatcher<bool>::finished, this, [this, watcher, savePathCopy]() {
        bool ok = watcher->result();
        watcher->deleteLater();
        m_xdgOpenForImageOperateInProgress = false;

        if (!ok) {
            KyInfo() << "xdgOpenForImageOperate failed: decode/write image failed";
            return;
        }

        xdgOpen(savePathCopy);
    });

    auto future = QtConcurrent::run([base64Image, savePathCopy]() -> bool {
        int commaIndex = base64Image.indexOf(',');
        if (commaIndex == -1) {
            return false;
        }

        // 去掉 data:image/*;base64, 前缀（避免主线程进行大块截断/拷贝）
        QByteArray decodedData = QByteArray::fromBase64(base64Image.mid(commaIndex + 1).toUtf8());
        QImage image;
        if (!image.loadFromData(decodedData)) {
            return false;
        }

        // 用扩展名推断格式保存（比 QImageWriter 在这里更简单）
        return image.save(savePathCopy);
    });
    watcher->setFuture(future);

}

void CMsgPane::reloadMeetingSummary(QString name)
{
    if(name.isEmpty())
    {
        return;
    }
    //判断数据库中是否存在此文件
    QString string = m_database->selectHumanMessagesByMeetingName(name);
    KyInfo() << "string" << string;

    //初始化会议助手限制
    meetingInhibit();

    if(!string.isEmpty()){
        //处理生成文本
        processMessageList(string);

    } else {
        saveEmptyMeetingToFile();
    }
}


// 初始化追问管理器
void CMsgPane::initFollowUpManager()
{
    m_followUpManager = new FollowUpManager(this);
    m_followUpManager->initialize();
    
    connect(m_followUpManager, &FollowUpManager::questionsGenerated,
            this, &CMsgPane::onAddQuestion);
    connect(m_followUpManager, &FollowUpManager::requestFailed,
            this, [this](const QString &error) {
        KyInfo() << "Follow up request failed:" << error;
    });

    return;
}

void CMsgPane::watchMeetingRecordDir()
{
    //观察会议记录文件夹
    QString dirPath = Globals::defaultPath + "/meeting";
    QDir dir(dirPath);
    if(!dir.exists())
    {
        KyInfo() << "meeting dir not exist";
        return;
    }
    QFileSystemWatcher *fileWatcher = new QFileSystemWatcher(this);
    fileWatcher->setObjectName("kylin-aiassistant_QFileSystemWatcher_m_fileWatcher");
    fileWatcher->addPath(dirPath);
    connect(fileWatcher, &QFileSystemWatcher::directoryChanged, this, &CMsgPane::flushMeetingFileList);
}

void CMsgPane::stopFollowUpRequest()
{
    KyInfo() << "stopFollowUpRequest called from UI";
    if (m_followUpManager) {
        m_followUpManager->stopFollowUpRequest();
    }
    return;
}

void CMsgPane::buildFollowUpRequest()
{
    if (m_followUpManager) {
        m_followUpManager->buildFollowUpRequest(recv_message, m_isUserStopped);
        // 重置用户停止标志，为下一次做准备
        if (m_isUserStopped) {
            m_isUserStopped = false;
        }
    }
    return;
}

int CMsgPane::getKnowledgeFileCountByFilepath(const QString filePath)
{
#ifdef KB_SUPPORT_ENABLED
    if(m_knowledgeBaseDateBase != nullptr) {
        int itemId = m_knowledgeBaseDateBase->getItemIdByDocumentargetPath(filePath);
        QList<int> list = m_knowledgeBaseDateBase->selectAllChildDirItemIdByFilepath(itemId);
        return m_knowledgeBaseDateBase->selectFileCountByFilePath(list, true);
    }
#endif
    return 0;
}

QStringList CMsgPane::getAllFileIdsByFilepath(const QString filePath)
{
#ifdef KB_SUPPORT_ENABLED
    if(m_knowledgeBaseDateBase != nullptr) {
        int itemId = m_knowledgeBaseDateBase->getItemIdByDocumentargetPath(filePath);
        QList<int> list = m_knowledgeBaseDateBase->selectAllChildDirItemIdByFilepath(itemId);
        return m_knowledgeBaseDateBase->selectAllFileIdsByFilepath(list);
    }
#endif
    return QStringList();
}

void CMsgPane::handleSendFile(const QString filePath)
{
    const QString absolutePath = toAbsoluteLocalFilePath(filePath);
    if (absolutePath.isEmpty()) {
        return;
    }

    QFileInfo fileInfo(absolutePath);
    if (g_supportPicFormats.contains(fileInfo.suffix().toLower())) {
        onShowImage(absolutePath, 1);
    } else if (g_supportDocFormats.contains(fileInfo.suffix().toLower())) {
        onShowDocument(absolutePath);
    }

    return;
}

int CMsgPane::getTextBit(const QString text)
{
    auto local8Bit = text.toLocal8Bit();
    return local8Bit.length();
}

bool CMsgPane::isContainDir(const QVariantList &strings)
{
    bool isContainDirFlag = false;
    for (const QVariant &elem : strings) {
        QFileInfo fileInfo(elem.toString());
        if(fileInfo.isDir()){
            return true;
        }
        if(elem.toString() == "trash:///" || elem.toString() == "computer:///"){
            return true;
        }
    }
    return isContainDirFlag;
}

bool CMsgPane::getIsIntroductionVisible()
{
    return mIsIntroductionVisible;
}

void CMsgPane::setIsIntroductionVisible(bool flag)
{
    mIsIntroductionVisible = flag;
}

int CMsgPane::getHistoryCount()
{
    return m_database->selectHistoryCount(0);
}

bool CMsgPane::getIsShowHistoryMaxTip()
{
    return ConfigManager::getInstance()->getConfigValue("is-show-history-max-tip").toBool();
}

void CMsgPane::setIsShowHistoryMaxTip(bool flag)
{
    ConfigManager::getInstance()->updateConfig("is-show-history-max-tip", flag);
}

void CMsgPane::deleteTheEarliestHistory()
{
    m_database->deleteTheEarliestHistory();
}

void CMsgPane::saveDialogToFile(const QString &dialogJsonString)
{
    // 获取当前时间
    QDateTime currentTime = QDateTime::currentDateTime();

    // 格式化当前时间为字符串，包含年、月、日、小时、分钟和秒
    QString formattedTime = currentTime.toString("yyyyMMdd_hhmmss");

    // 使用格式化后的时间作为文件名
    QString fileName = Globals::defaultPath + "/.dialog/dialog_" + formattedTime + ".json";

    QFile file(fileName);
    if(!file.open(QIODevice::WriteOnly | QIODevice::Text))
    {
        return;
    }

    file.write(dialogJsonString.toUtf8());

    file.close();

    // 由于CMsgPane::saveDialogToFile是被异步调用的，所以这里通过信号通知历史会话界面当前已经保存了会话到文件，以此更新历史会话界面。
    Q_EMIT sigSaveDialog(fileName);

    return;
}

void CMsgPane::saveMessageToFile(const QString &msgJsonString)
{
    QString fileName = Globals::defaultPath + "/.dialog/message.json";

    QFile file(fileName);
    if(!file.open(QIODevice::WriteOnly | QIODevice::Text))
    {
        return;
    }

    file.write(msgJsonString.toUtf8());

    file.close();

    return;
}

void CMsgPane::saveMeetingToFile(QString model)
{
    Q_UNUSED(model);
    // 会议助手功能已下线，不再将会议内容写入文件
    m_summary.clear();
    m_origin.clear();
}

void CMsgPane::saveEmptyMeetingToFile()
{
    // 会议助手功能已下线，不再写入空会议文件
    m_summary.clear();
    m_origin.clear();
}

bool CMsgPane::meetingInhibit()
{
    //会议助手运行时禁止锁屏
    meetingLockScreenReply = m_meetingInhibitLockScreen->call(QDBus::Block, "Inhibit", "kylin-aiassistant", 0, "Ai助手正在执行", 8);
    isMeetingInhibit = true;
    return true;
}

void CMsgPane::meetingUnInhibit()
{
    //当需要解除抑制时，调用 UnInhibit
    if(m_meetingInhibitLockScreen && meetingLockScreenReply.arguments().size() != 0){
        m_meetingInhibitLockScreen->call("Uninhibit",meetingLockScreenReply.arguments().takeFirst().toUInt());
    }
    isMeetingInhibit = false;
    KyInfo() <<  "解除会议助手运行抑制" << isMeetingInhibit;

}

void CMsgPane::recordingInhibit()
{
    // 录音时禁止自动锁屏和睡眠
    if (!m_meetingInhibitLockScreen) {
        return;
    }
    recordingLockScreenReply = m_meetingInhibitLockScreen->call(
        QDBus::Block, "Inhibit",
        "kylin-aiassistant", quint32(0), "小K正在录音", quint32(8));
    KyInfo() << "录音抑制已添加";
}

void CMsgPane::recordingUnInhibit()
{
    if (m_meetingInhibitLockScreen && recordingLockScreenReply.arguments().size() != 0) {
        m_meetingInhibitLockScreen->call("Uninhibit",
            recordingLockScreenReply.arguments().takeFirst().toUInt());
        KyInfo() << "录音抑制已解除";
    }
}

void CMsgPane::readingInhibit()
{
    // 朗读时禁止自动锁屏和睡眠
    if (!m_meetingInhibitLockScreen) {
        return;
    }
    readingLockScreenReply = m_meetingInhibitLockScreen->call(
        QDBus::Block, "Inhibit",
        "kylin-aiassistant", quint32(0), "小K正在朗读", quint32(8));
    KyInfo() << "朗读抑制已添加";
}

void CMsgPane::readingUnInhibit()
{
    if (m_meetingInhibitLockScreen && readingLockScreenReply.arguments().size() != 0) {
        m_meetingInhibitLockScreen->call("Uninhibit",
            readingLockScreenReply.arguments().takeFirst().toUInt());
        KyInfo() << "朗读抑制已解除";
    }
}

void CMsgPane::saveDialog()
{
    // 先保存当前会话
//    m_pViewChat->page()->runJavaScript("getActiveDialog()", [this](const QVariant &v) {
//        this->saveDialogToFile(v.toString());
//    });

    return;
}

void CMsgPane::clearDialog()
{
    // 然后清除当前会话
//    m_pViewChat->page()->runJavaScript("clearActiveDialog()");

    return;
}

void CMsgPane::loadPlugin(QString pluginHtmlFile)
{
//    m_pViewChat->load(QUrl(pluginHtmlFile));
}


///

///
/// \brief CMsgPane::openSchedule
/// 通过日历D-Bus接口打开日历
/// \param todoListJson 待办列表JSON字符串，格式为 [{"currentYear":2025,"currentMonth":3,"currentDay":25,"content":"待办内容"},...]，从第一个待办项解析日期
///
void CMsgPane::openSchedule(QString todoListJson)
{
    KyInfo() << "openSchedule todoListJson: " << todoListJson;

    // 解析JSON字符串，从第一个待办项提取日期
    int year = 0, month = 0, day = 0;
    if (!todoListJson.isEmpty()) {
        QJsonParseError error;
        QJsonDocument doc = QJsonDocument::fromJson(todoListJson.toUtf8(), &error);
        if (error.error == QJsonParseError::NoError && doc.isArray()) {
            QJsonArray jsonArray = doc.array();
            // 只解析第一个待办项的日期
            if (!jsonArray.isEmpty()) {
                QJsonValue firstValue = jsonArray.at(0);
                if (firstValue.isObject()) {
                    QJsonObject obj = firstValue.toObject();
                    year = obj["currentYear"].toInt(0);
                    month = obj["currentMonth"].toInt(0);
                    day = obj["currentDay"].toInt(0);
                    KyInfo() << "Parsed date from first todo: " << year << "-" << month << "-" << day;
                }
            }
        } else {
            KyWarning() << "Failed to parse todoListJson:" << error.errorString();
        }
    }

    // 如果解析失败，使用当前日期
    if (year <= 0 || month <= 0 || day <= 0) {
        KyWarning() << "No valid date found in todoListJson, using current date";
        QDate currentDate = QDate::currentDate();
        year = currentDate.year();
        month = currentDate.month();
        day = currentDate.day();
    }

    // 创建D-Bus接口
    QDBusInterface *calendarInterface = new QDBusInterface(
        "org.ukui.calendar.Notifications",
        "/org/ukui/calendar/Notifications",
        "org.ukui.calendar.Notifications",
        QDBusConnection::sessionBus(),
        this
    );
    
    if (!calendarInterface->isValid()) {
        KyWarning() << "Failed to create D-Bus interface for calendar:" 
                    << QDBusConnection::sessionBus().lastError().message();
        delete calendarInterface;
        return;
    }
    
    // 调用OpenSchedule方法打开日历并跳转到指定日期
    QDBusReply<bool> reply = calendarInterface->call(
        "OpenSchedule",
        day, month, year
    );
    
    if (!reply.isValid()) {
        KyWarning() << "OpenSchedule call failed:" << reply.error().message();
    } else {
        QString schedule_date = QString("%1-%2-%3")
            .arg(year, 4, 10, QChar('0'))
            .arg(month, 2, 10, QChar('0'))
            .arg(day, 2, 10, QChar('0'));
        KyInfo() << "Successfully opened calendar for date: " << schedule_date;
    }
    
    delete calendarInterface;
}

///
/// \brief CMsgPane::addTodoToSchedule
/// 通过ScheduleNotification接口将待办添加到日程
/// \param todoListJson 待办事项JSON字符串，数组元素包含日期和内容字段等，格式为 [{"currentYear":2025,"currentMonth":3,"currentDay":25,"content":"待办内容"},...]
///
void CMsgPane::addTodoToSchedule(QString todoListJson)
{
    KyInfo() << "addTodoToSchedule " << todoListJson;

    // 创建D-Bus接口（复用，避免重复创建）
    QDBusInterface *notificationInterface = new QDBusInterface(
        "org.ukui.calendar.Notifications",
        "/org/ukui/calendar/Notifications",
        "org.ukui.calendar.Notifications",
        QDBusConnection::sessionBus(),
        this
    );
    
    if (!notificationInterface->isValid()) {
        KyWarning() << "Failed to create D-Bus interface for ScheduleNotification:" 
                    << QDBusConnection::sessionBus().lastError().message();
        delete notificationInterface;
        return;
    }
    
    QJsonParseError parseError;
    QJsonDocument inputDoc = QJsonDocument::fromJson(todoListJson.toUtf8(), &parseError);
    if (parseError.error != QJsonParseError::NoError || !inputDoc.isArray()) {
        KyWarning() << "Invalid todo list json:" << parseError.errorString();
        delete notificationInterface;
        return;
    }

    QJsonArray array = inputDoc.array();
    for (const QJsonValue &item : array) {
        if (!item.isObject()) {
            continue;
        }

        QJsonObject obj = item.toObject();
        int hour = obj.value("hour").toInt(0);
        int minute = obj.value("minute").toInt(0);
        int currentDay = obj.value("currentDay").toInt(0);
        int currentMonth = obj.value("currentMonth").toInt(0);
        int currentYear = obj.value("currentYear").toInt(0);
        int endMinute = obj.value("endMinute").toInt(0);
        int endHour = obj.value("endHour").toInt(0);
        QString todoItem = obj.value("content").toString();

        if (currentYear <= 0 || currentMonth <= 0 || currentDay <= 0 || todoItem.isEmpty()) {
            KyWarning() << "Skip invalid todo item, missing date or content";
            continue;
        }

        QDBusReply<bool> reply = notificationInterface->call(
            "ScheduleNotification",
            hour, minute, currentDay, currentMonth, currentYear,
            endMinute, endHour,
            todoItem
        );

        if (!reply.isValid()) {
            KyWarning() << "ScheduleNotification call failed for todo '" << todoItem 
                        << "':" << reply.error().message();
        } else {
            KyInfo() << "Successfully added schedule: " << todoItem << " on " 
                     << currentYear << "-" << currentMonth << "-" << currentDay;
        }
    }

    delete notificationInterface;
}

// 解析待办时间
static bool parseTodoTimeToken(const QString &timeText, int &hour, int &minute, QString *resolvedPeriod = nullptr, const QString &defaultPeriod = QString())
{
    QRegularExpression timeRegex(
        R"(^(上午|下午|中午|晚上|凌晨)?\s*(\d{1,2})(?:(?:\s*[:：]\s*(\d{1,2}))|(?:\s*(?:点|时)\s*(\d{1,2})?\s*分?))?\s*$)"
    );
    QRegularExpressionMatch match = timeRegex.match(timeText.trimmed());
    if (!match.hasMatch()) {
        return false;
    }

    QString period = match.captured(1).trimmed();
    if (period.isEmpty()) {
        period = defaultPeriod;
    }

    hour = match.captured(2).toInt();
    minute = 0;
    if (!match.captured(3).isEmpty()) {
        minute = match.captured(3).toInt();
    } else if (!match.captured(4).isEmpty()) {
        minute = match.captured(4).toInt();
    }

    if (minute < 0 || minute > 59 || hour < 0 || hour > 23) {
        return false;
    }

    if (period == "下午" || period == "晚上") {
        if (hour < 12) {
            hour += 12;
        }
    } else if (period == "中午") {
        if (hour >= 1 && hour <= 10) {
            hour += 12;
        }
    } else if (period == "凌晨") {
        if (hour == 12) {
            hour = 0;
        }
    }

    if (hour < 0 || hour > 23) {
        return false;
    }

    if (resolvedPeriod != nullptr) {
        *resolvedPeriod = period;
    }

    return true;
}

// 提取代办时间范围
static bool extractTodoTimeRange(QString &todoItem, int &hour, int &minute, int &endHour, int &endMinute)
{
    QString originalTodoItem = todoItem;

    QRegularExpression timeRangeRegex(
        R"(((?:上午|下午|中午|晚上|凌晨)?\s*\d{1,2}(?:(?:\s*[:：]\s*\d{1,2})|(?:\s*(?:点|时)\s*\d{0,2}\s*分?))?)\s*(?:到|至|-|~|～|—|－)\s*((?:上午|下午|中午|晚上|凌晨)?\s*\d{1,2}(?:(?:\s*[:：]\s*\d{1,2})|(?:\s*(?:点|时)\s*\d{0,2}\s*分?))?))"
    );
    QRegularExpressionMatch match = timeRangeRegex.match(todoItem);

    if (match.hasMatch()) {
        QString startToken = match.captured(1).trimmed();
        QString endToken = match.captured(2).trimmed();
        QString startPeriod;
        int parsedHour = 0;
        int parsedMinute = 0;
        int parsedEndHour = 0;
        int parsedEndMinute = 0;

        if (!parseTodoTimeToken(startToken, parsedHour, parsedMinute, &startPeriod)) {
            return false;
        }

        if (!parseTodoTimeToken(endToken, parsedEndHour, parsedEndMinute, nullptr, startPeriod)) {
            return false;
        }

        todoItem.remove(match.capturedStart(0), match.capturedLength(0));
        todoItem = todoItem.trimmed();
        todoItem.remove(QRegularExpression(R"(^[：:，,、\-\s]+)"));
        todoItem = todoItem.trimmed();

        if (todoItem.isEmpty()) {
            todoItem = originalTodoItem;
            return false;
        }

        hour = parsedHour;
        minute = parsedMinute;
        endHour = parsedEndHour;
        endMinute = parsedEndMinute;
        return true;
    }

    // 如果没有时间范围，则尝试匹配单个时间点
    QRegularExpression singleTimeRegex(
        R"((上午|下午|中午|晚上|凌晨)?\s*(\d{1,2})(?:(?:\s*[:：]\s*(\d{1,2}))|(?:\s*(?:点|时)\s*(\d{0,2})\s*分?))?)",
        QRegularExpression::MultilineOption
    );

    match = singleTimeRegex.match(todoItem);
    if (match.hasMatch()) {
        QString period = match.captured(1).trimmed();
        int parsedHour = match.captured(2).toInt();
        int parsedMinute = 0;

        if (!match.captured(3).isEmpty()) {
            parsedMinute = match.captured(3).toInt();
        } else if (!match.captured(4).isEmpty()) {
            parsedMinute = match.captured(4).toInt();
        }

        // 处理上午/下午
        if (period == "下午" || period == "晚上") {
            if (parsedHour < 12) {
                parsedHour += 12;
            }
        } else if (period == "中午") {
            if (parsedHour >= 1 && parsedHour <= 10) {
                parsedHour += 12;
            }
        }

        // 清理时间文本
        todoItem.remove(match.capturedStart(0), match.capturedLength(0));
        todoItem = todoItem.trimmed();
        todoItem.remove(QRegularExpression(R"(^[：:，,、\-\s]+)"));
        todoItem = todoItem.trimmed();

        if (todoItem.isEmpty()) {
            todoItem = originalTodoItem;
            return false;
        }

        // 设置时间：开始时间为解析的时间，结束时间默认加1小时
        hour = parsedHour;
        minute = parsedMinute;
        endHour = (parsedHour + 1) % 24;
        endMinute = parsedMinute;

        return true;
    }

    return false;
}

// 解析待办日期文本
static bool parseTodoDatePart(const QString &datePart, int &year, int &month, int &day)
{
    const QString normalizedDatePart = datePart.trimmed();
    if (normalizedDatePart.isEmpty()) {
        return false;
    }

    QRegularExpression chineseDateRegex(R"((\d{4})年(\d{1,2})月(\d{1,2})日)");
    QRegularExpressionMatch chineseMatch = chineseDateRegex.match(normalizedDatePart);
    if (chineseMatch.hasMatch()) {
        year = chineseMatch.captured(1).toInt();
        month = chineseMatch.captured(2).toInt();
        day = chineseMatch.captured(3).toInt();
        return year > 0 && month > 0 && day > 0;
    }

    QRegularExpression dottedDateRegex(R"((\d{4})[.\-/](\d{1,2})[.\-/](\d{1,2}))");
    QRegularExpressionMatch dottedMatch = dottedDateRegex.match(normalizedDatePart);
    if (dottedMatch.hasMatch()) {
        year = dottedMatch.captured(1).toInt();
        month = dottedMatch.captured(2).toInt();
        day = dottedMatch.captured(3).toInt();
        return year > 0 && month > 0 && day > 0;
    }

    const QStringList isoParts = normalizedDatePart.split("-");
    if (isoParts.size() >= 3) {
        year = isoParts[0].toInt();
        month = isoParts[1].toInt();
        day = isoParts[2].toInt();
        return year > 0 && month > 0 && day > 0;
    }

    return false;
}

QString CMsgPane::getTodoList(QString todoMessage)
{
    KyInfo() << "todoMessage " << todoMessage;
    QVariantList todoList;  // 用于返回的待办列表

    QString plainMessage = todoMessage;
    plainMessage.remove(QRegularExpression("<[^>]+>"));

    // 判断输入是日期格式（"2025-03-25"）还是待办事项消息
    // 如果包含 "# " 开头，说明是待办事项格式，需要解析
    if ((plainMessage.contains("## ") || plainMessage.contains("# ")) && plainMessage.contains("- ")) {
        // 解析待办事项格式: "## 日期 星期\n- 待办事项1\n- 待办事项2\n"
        QStringList lines = plainMessage.split("\n", QString::SkipEmptyParts);
        int currentYear = 0;
        int currentMonth = 0;
        int currentDay = 0;

        for (const QString &line : lines) {
            QString trimmedLine = line.trimmed();

            // 检查是否是日期行（格式: "## 日期 星期" 或 "# YYYY年MM月DD日 星期"）
            if (trimmedLine.startsWith("## ") || trimmedLine.startsWith("# ")) {
                // 提取日期部分（去掉 "#"/"##" 前缀和星期部分）
                QString datePart = trimmedLine.startsWith("## ")
                        ? trimmedLine.mid(3).trimmed()
                        : trimmedLine.mid(2).trimmed();
                // 移除星期部分（星期可能在日期后面，用空格分隔）
                int spaceIndex = datePart.indexOf(" ");
                if (spaceIndex > 0) {
                    datePart = datePart.left(spaceIndex);
                }

                if (parseTodoDatePart(datePart, currentYear, currentMonth, currentDay)) {
                    KyInfo() << "Parsed date: " << currentYear << "-" << currentMonth << "-" << currentDay;
                } else {
                    currentYear = 0;
                    currentMonth = 0;
                    currentDay = 0;
                }
            }
            // 检查是否是待办事项行（格式: "- 待办事项"）
            else if (trimmedLine.startsWith("- ") && currentYear > 0) {
                QString todoItem = trimmedLine.mid(2).trimmed();
                if (!todoItem.isEmpty()) {
                    int todoHour = 0;
                    int todoMinute = 0;
                    int todoEndHour = 0;
                    int todoEndMinute = 0;
                    extractTodoTimeRange(todoItem, todoHour, todoMinute, todoEndHour, todoEndMinute);

                    // 添加到返回列表
                    QVariantMap todoMap;
                    todoMap["hour"] = todoHour;
                    todoMap["minute"] = todoMinute;
                    todoMap["currentDay"] = currentDay;
                    todoMap["currentMonth"] = currentMonth;
                    todoMap["currentYear"] = currentYear;
                    todoMap["endMinute"] = todoEndMinute;
                    todoMap["endHour"] = todoEndHour;
                    todoMap["content"] = todoItem;
                    todoList.append(todoMap);
                }
            }
        }
    }

    // 将QVariantList转换为JSON字符串
    QJsonArray jsonArray;
    for (const QVariant &item : todoList) {
        QVariantMap map = item.toMap();
        QJsonObject jsonObj;
        jsonObj["hour"] = map["hour"].toInt();
        jsonObj["minute"] = map["minute"].toInt();
        jsonObj["currentDay"] = map["currentDay"].toInt();
        jsonObj["currentMonth"] = map["currentMonth"].toInt();
        jsonObj["currentYear"] = map["currentYear"].toInt();
        jsonObj["endMinute"] = map["endMinute"].toInt();
        jsonObj["endHour"] = map["endHour"].toInt();
        jsonObj["content"] = map["content"].toString();

        jsonArray.append(jsonObj);
    }

    QJsonDocument doc(jsonArray);
    QString jsonString = doc.toJson(QJsonDocument::Compact);
    KyInfo() << "getTodoList return JSON: " << jsonString;

    return jsonString;
}

void CMsgPane::onClearDictation()
{
}

void CMsgPane::onCopySuccess()
{
    Q_EMIT copySuccess();
}

// 过滤掉带圈数字角标的辅助函数
static QString filterChunkIndex(const QString &text)
{
    QString filteredText = text;
    // 使用 QChar 范围匹配，移除 ❶❷❸...➉ 这些角标字符（Unicode 0x2776-0x2789）
    for (int i = text.length() - 1; i >= 0; --i) {
        ushort unicode = text.at(i).unicode();
        if (unicode >= 0x2460 && unicode <= 0x2470) {
            filteredText.remove(i, 1);
        }
    }
    return filteredText;
}

void CMsgPane::onCopyText(const QString &text)
{
    QString filteredText = filterChunkIndex(text);

    QClipboard *selectClip = QApplication::clipboard();
    selectClip->setText(filteredText);

    Q_EMIT copySuccess();
}

void CMsgPane::copyTextFiltered(const QString &text)
{
    QString filteredText = filterChunkIndex(text);

    QClipboard *selectClip = QApplication::clipboard();
    selectClip->setText(filteredText);

    Q_EMIT copySuccess();
}

bool CMsgPane::isCurrentModelAvailable()
{
    if (m_chat == nullptr) {
        KyInfo() << "get isCurrentModelAvailable failed! m_chat init failed!";
        return false;
    }

    QString currentModel = getNLPCurModel();

    // 获取当前可用的模型列表
    QList<ModelItem *> availableModels = m_chat->getNLPModelList();

    // 检查当前模型是否在可用列表中
    for (auto item : availableModels) {
        if (item->m_modelName == currentModel) {
            return true;  // 找到了，模型可用
        }
    }
    return false;
}


void CMsgPane::onCopyImage(const QString &base64Image)
{
    if (m_copyImageToClipboardInProgress) {
        KyInfo() << "copy image already in progress, ignore duplicate request";
        return;
    }
    if (base64Image.isEmpty()) {
        return;
    }

    m_copyImageToClipboardInProgress = true;

    // 由于 base64 解码 + QImage::loadFromData 进行“像素解码/加载”可能很耗时，
    // 这里改为：后台只把 base64 解码成“原始图片字节”，主线程仅设置剪贴板 mimeData。
    auto *watcher = new QFutureWatcher<ClipboardImagePayload>(this);
    connect(watcher, &QFutureWatcher<ClipboardImagePayload>::finished, this, [this, watcher]() {
        ClipboardImagePayload payload = watcher->result();
        watcher->deleteLater();
        m_copyImageToClipboardInProgress = false;

        if (payload.data.isEmpty() || payload.mimeType.isEmpty()) {
            KyInfo() << "copy image: decode to bytes failed";
            return;
        }

        // 复制到剪切板（主线程执行）
        // 说明：setMimeData 可能仍涉及与剪贴板管理器交互；延后到下一轮事件循环，减少抢占 UI 绘制。
        QTimer::singleShot(0, this, [this, payload]() mutable {
            QClipboard *clipBoard = QApplication::clipboard();
            QMimeData *mimeData = new QMimeData();
            mimeData->setData(payload.mimeType, payload.data);
            clipBoard->setMimeData(mimeData, QClipboard::Clipboard);
            KyInfo() << "复制图片到剪贴板";
            Q_EMIT copyImageToClipboard();
        });
    });

    auto future = QtConcurrent::run([base64Image]() -> ClipboardImagePayload {
        ClipboardImagePayload payload;
        int commaIndex = base64Image.indexOf(',');
        if (commaIndex == -1) {
            return payload;
        }

        // 解析 mime type：data:image/png;base64,xxxxx
        // 兜底：如果解析失败，默认 image/png。
        int prefixStart = base64Image.indexOf("data:");
        if (prefixStart == 0) {
            int mimeEnd = base64Image.indexOf(';', prefixStart + 5);
            if (mimeEnd > prefixStart + 5) {
                payload.mimeType = base64Image.mid(prefixStart + 5, mimeEnd - (prefixStart + 5)).trimmed();
            }
        }
        if (payload.mimeType.isEmpty()) {
            payload.mimeType = QStringLiteral("image/png");
        }

        // 去掉 data:image/*;base64, 前缀部分，仅解码 base64 payload 为原始图片字节。
        QByteArray decodedData = QByteArray::fromBase64(base64Image.mid(commaIndex + 1).toUtf8());
        payload.data = decodedData;
        return payload;
    });
    watcher->setFuture(future);

}

void CMsgPane::onLikeAction()
{
    Q_EMIT sigLikeAction();
}

void CMsgPane::onsigReloadIsRunning()
{
    QMetaObject::invokeMethod(this,"onTipsReloadIsRunning",Qt::QueuedConnection);

}

void CMsgPane::onReloadIsRunning()
{
}

void CMsgPane::onReInitMeetingNlpSpeech()
{
    // 会议助手功能已下线，此接口保留为空实现以兼容旧调用
}

//进入会议助手时，开始录音逻辑
void CMsgPane::onSendMeetingAudio()
{
    // 会议助手功能已下线，不再启动会议录音
}

void CMsgPane::onPauseMeetingAudio()
{
    // 会议助手功能已下线，空实现
}

void CMsgPane::onContinueMeetingAudio()
{
    // 会议助手功能已下线，空实现
}

void CMsgPane::onStopMeetingAudio()
{
    // 会议助手功能已下线，空实现
}

void CMsgPane::flushMeetingFileList()
{
    //刷新会议文件列表
    getMeetingList();
}

void CMsgPane::processMessageList(QString jsonStr)
{
    //    m_chat->target = SUMMARY;
    // 会议助手功能已移除
    //解析jsonStr，提取content的值，拼接成QString字符串，赋值给m_origin
    QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonStr.toUtf8());
    if(!jsonDoc.isNull())
    {
        if(jsonDoc.isArray())
        {
            QJsonArray jsonArray = jsonDoc.array();
            for(const QJsonValue &value: jsonArray)
            {
                if(value.isObject())
                {
                    QJsonObject jsonObject = value.toObject();
                    if(jsonObject.contains("name"))
                    {
                        QString name = jsonObject.value("name").toString();
                        m_origin.append(name + " ");
                    }
                    if(jsonObject.contains("start") && jsonObject.contains("end"))
                    {
                        QString time = jsonObject.value("start").toString() + "-" + jsonObject.value("end").toString();
                        m_origin.append(time + "\n");
                    }
                    if(jsonObject.contains("content"))
                    {
                        QString content = jsonObject.value("content").toString();
                        m_origin.append(content + "\n");
                    }
                }
            }
        }
    }
    ////如果解析出的总结内容中为空，则应当添加空值标志位，方便进行总结和生成时的判断

    //如果解析总结内容为空，则直接写入空文件并返回，无需进行总结
    if(m_origin.isEmpty())
    {
        saveEmptyMeetingToFile();
    }
    else
    {
    //否则则走发送总结流程并进行处理
    int meetingSummaryId = m_commandListModel->getPromptId("总结概括");    //设置会议总结角色，发送会议原文
    //m_chat->setActor(meetSummaryId);
    //m_chat->sendMessage(m_origin, nullptr);

    // 会议助手功能已移除，相关调用已删除
    }
}
void CMsgPane::onSaveMessageList()
{
    //跳转前更新保存当前会话内容
//    m_pViewChat->page()->runJavaScript("getActiveDialog()", [this](const QVariant &v) {
//        this->saveMessageToFile(v.toString());
//    });
}

void CMsgPane::onLoadMessageList()
{
    QString directoryPath = Globals::defaultPath + "/.dialog";
    QDir dir(directoryPath);
    if(!dir.exists())
    {
        KyInfo() << "目录不存在：" << directoryPath;
        return;
    }
    QString fileName = "message.json";
    QFile file(directoryPath + "/" + fileName);
    if(!file.open(QIODevice::ReadOnly | QIODevice::Text))
    {
        KyInfo() << "无法打开文件：" << fileName;
        return;
    }

    QByteArray jsonData = file.readAll();
    file.close();

    QJsonParseError parseError;
    QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parseError);
    if(parseError.error != QJsonParseError::NoError)
    {
        KyInfo() << "JSON 解析错误：" << parseError.errorString();
        return;
    }

    QJsonArray jsonArray = jsonDoc.array();
    QString str = QJsonDocument(jsonArray).toJson(QJsonDocument::Indented);
    str.replace("\\", "\\\\");
    str.replace("\n", "\\n");
    str.replace("\"", "\\\"");
    QString js_str = QString("loadMessageList('%1')").arg(str);
    //    m_pViewChat->page()->runJavaScript(js_str);
}

void CMsgPane::onChangedIsReplyingStatus(bool isReplying)
{
    m_isReplying = isReplying;
    KyInfo() << "update m_isReplying " << m_isReplying;

    if(!isReplying) {
        // 如果没有在等待回答了，则需要将超时检测定时器停止
        if (m_chat == nullptr) {
            KyInfo() << "m_chat is not inited.";
            return;
        }

#ifdef KB_SUPPORT_ENABLED
        if(m_KBChat == nullptr) {
            KyInfo() << "m_KBChat is not inited.";
            return;
        }
#endif
    }

    return;
}


///
/// \brief CMsgPane::onRecvKBMsg 用于处理知识库问答的返回消息
/// \param msg  消息体
/// \param errorCode 错误码
///
#ifdef KB_SUPPORT_ENABLED
void CMsgPane::onKBRecvMsg(KbChatMessage *msg, int errorCode)
{
    // 错误处理
    if (errorCode != static_cast<int>(AI_COMMON_SUCCESS)) {
        //如果上一条消息正在显示，则调用停止回答接口
        if (recv_message != "") {
            recv_message = "";
            recv_reasoning_message = "";
            m_recv_chunkIndexMap.clear();
        }

        m_chatType = NORMAL;

        // 特殊的报错需要单独处理
        QString errorMsg = msg->m_content;


        if(errorCode != AI_COMMON_UNKNOWN_ERROR){
            //对于未知错误，展示error_message字段对应的所有值   10000错误码显示sdk返回的报错信息
            errorMsg = Globals::getKnowledgeChatErrorMsg(errorCode);
        }

        // 特殊的报错需要单独处理
        if(errorCode == static_cast<int>(KB_ERROR_KB_EMPTY)){
            QJsonObject jsonObj;
            // 知识库无可用文件提示
            jsonObj["modelMsg"] = "ai";
            jsonObj["feedbackURL"] = msg->m_feedbackURL;
            jsonObj["chatType"] = KNOWLEDGEBASE;
            QJsonDocument aiJsonSendWebDoc(jsonObj);
            QString jsonMsgHeadToWeb = aiJsonSendWebDoc.toJson(QJsonDocument::Indented);
            errorMsg = tr("There are no referenced files in the current knowledge base, unable to generate valid answers. Please add files and try again.");
            pushMsg("updateMsg", jsonMsgHeadToWeb, errorMsg);
            pushMsg("replyEnd", true);
            clearJsonMessage();
        }else if(errorCode == static_cast<int>(AI_COMMON_MODEL_NOT_FOUND)){
            errorMsg = tr("<span style=\"color:red\">There are currently no available large language model available.</span> <br><br> Configuration steps: <br> 1. Go to the <strong>System Settings &gt; AI Module Management &gt; Model Configuration</strong> page to add a \"cloud model\", a \"local model\" or a \"optional model\". <br> 2. If the model configuration is already completed, please confirm whether the model is enabled. <br><br> Note: Running a local model may consume significant device resources. Please ensure your device meets the performance requirements.");
            // 知识库无可用文本类型提示
            errorMessageToKbJs("", "ai", "black", errorMsg, KNOWLEDGEBASE, "true");
        }else if(errorCode ==  static_cast<int>(KB_ERROR_RETRIEVAL_NO_RESULTS)){
            // 知识库检索无结果提示，使用黑色文字
            errorMessageToKbJs("", "ai", "black", errorMsg, KNOWLEDGEBASE);
        }/*else if(errorCode == static_cast<int>(AI_COMMON_FREE_ALLOCATED_QUOTA_EXCEEDED)){
            // 拼接红色异常提示和正常黑色提示文字
            errorMsg = "<span style=\"color:red\">" + errorMsg + "</span>"+ "<br>" + tr("To ensure the normal use of the AI assistant, please go to System Settings > AI Module Management > Model Configuration to configure the model.");
            errorMessageToKbJs("", "ai", "black", errorMsg, KNOWLEDGEBASE);
        }*/else if(errorCode == static_cast<int>(KB_ERROR_RETRIEVAL_FILEID_FILTER_INVALID)){
            // 知识库检索时文件已被删除，与临时文件处理一致，使用红色文字提示
            errorMsg = tr("There are deleted files in the selected file. Please select again and try again.");
            errorMessageToKbJs("", "ai", "red", errorMsg, KNOWLEDGEBASE);
        }else{
            errorMessageToKbJs("", "ai", "red", errorMsg, KNOWLEDGEBASE);
        }
        clearJsonMessage();

        // 当知识库回答结束，添加回答到上下文
        m_KBChat->kbChatHistoryAddResponse(errorMsg);

        // 系统运维提示框
        if(errorCode ==  static_cast<int>(KB_ERROR_KB_EMPTY)){
            // 知识库无可用文件系统运维提示框
            QTimer *tempTimer = new QTimer(this);
            connect(tempTimer,&QTimer::timeout,this,[&]{
                systemMessageToJs("", "ai", "black", tr("Knowledge Base"), "qrc:/icon/icon/knowledge-base-setting.svg", tr("Knowledge Base"), 1, KNOWLEDGEBASE);
            });
            tempTimer->setSingleShot(true);
            tempTimer->start(200);
        }else if(errorCode ==  static_cast<int>(AI_COMMON_MODEL_NOT_FOUND)){
            // 知识库无可用文本类型系统运维提示框
            QTimer *tempTimer = new QTimer(this);
            connect(tempTimer,&QTimer::timeout,this,[&]{
                systemMessageToJs("", "ai", "black", tr("Setting"), "qrc:/icon/icon/setting.png", tr("Model configuration"), 0, KNOWLEDGEBASE);
            });
            tempTimer->setSingleShot(true);
            tempTimer->start(200);
        }else{
        }
        clearJsonMessage();

        new_reply = true;

        return;
    }

    if (msg == nullptr) {
        qInfo() << " onKBRecvMsg msg is nullptr";

        return;
    }

    jsonObjToWeb["modelMsg"] = (msg->m_isEnd == true && msg->m_model == "") ? "ai" : msg->m_model;

    jsonObjToWeb["feedbackURL"] = msg->m_feedbackURL;
    // 添加知识库问答标识
    jsonObjToWeb["chatType"] = KNOWLEDGEBASE;
    QJsonDocument aiJsonSendWebDoc(jsonObjToWeb);
    QString jsonMsgHeadToWeb = aiJsonSendWebDoc.toJson(QJsonDocument::Indented);
    //如果是新消息的第一个回答，设置消息头，清空文本，准备显示正文
    if (new_reply) {
        pushMsg("updateMsg", jsonMsgHeadToWeb, "");
        clearJsonMessage();
    }
    recv_message.append(msg->m_content);
    recv_reasoning_message.append(msg->m_reasonContent);
    QMap<int, int>::const_iterator it = msg->m_chunkIndexMap->constBegin();
    while (it != msg->m_chunkIndexMap->constEnd()) {
        int key = it.key();      // 获取键
        int value = it.value();  // 获取值
        m_recv_chunkIndexMap.insert(key, value);
        ++it;
    }

    // 思考过程带html标签，需先转义
    recv_reasoning_message.replace("<","&lt;");
    recv_reasoning_message.replace(">","&gt;");

    new_reply = msg->m_isEnd;
    // 当知识库回答结束，添加回答到上下文
    if(msg->m_isEnd){
        m_KBChat->kbChatHistoryAddResponse(recv_message);
    }
    // 当知识库回答结束时，推送信息来源（文档标题、类型、路径）到前端
    if (msg->m_isEnd && msg->m_fileIds.size() > 0) {
        QJsonObject jsonObj;
        QJsonArray jsonArray;
        QSet<QString> seenGuideApps; // 去重同一应用的用户手册路径
        for (int i = 0; i < msg->m_fileIds.size(); i++) {
            const QString  fileIdPath = msg->m_fileIds.at(i);
            int chunkIndex = m_recv_chunkIndexMap.value(i + 1);
            QString content = ""; // 文件描述内容
            if(chunkIndex >= 0 && msg->m_fileContents.size() > 0 && chunkIndex <= msg->m_fileContents.size()){
                content = msg->m_fileContents.at(chunkIndex);
            }

            QString title;
            QString type;
            QString documentPath;

            // 系统指南的UUID
            QString systemGuideUuid = "550e8400-e29b-41d4-a716-446655440002";
            // 1. 指令是系统指南时（promptId==4或使用新指令系统的系统指南UUID），进行路径解析
            bool isSystemGuideChat = (promptId == 4 || commandUuid == systemGuideUuid);
            int infoAppId = -1;
            QString appName;
            if (isSystemGuideChat) {
                // 2. 解析fileIdPath获取映射信息
                InformationSource source = getInformationSourceFromUserGuidePath(fileIdPath);
                //解析存在FAQ场景
                // FAQ场景不显示信息来源，直接跳过
                if (source.id == -2) {
                    continue;
                }
                if (source.id != -1) {
                    // 3. 覆盖title和type值
                    title = source.url;
                    type = QString::number(source.id);
                    infoAppId = source.id;
                    // appName 与路径中的目录名一致
                    QString tempGuidePath;
                    if (fileIdPath.startsWith(userGuidePathPrefix)) {
                        tempGuidePath = userGuidePathPrefix;
                    } else if (fileIdPath.startsWith(kaimingUserGuidePathPrefix)) {
                        tempGuidePath = kaimingUserGuidePathPrefix;
                    }
                    QString tempPath = fileIdPath.mid(tempGuidePath.length());
                    int pos = tempPath.indexOf("/");
                    if (pos > 0) {
                        appName = tempPath.left(pos);
                    }
                }
                // 4. 系统指南场景按 appName 去重
                if (!appName.isEmpty()) {
                    if (seenGuideApps.contains(appName)) {
                        continue; // 已处理过该应用，跳过
                    }
                    seenGuideApps.insert(appName);
                }
            }else {
                // 非系统指南场景：判断fileIdPath是否是uuid格式
                // UUID格式：xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                QRegularExpression uuidRegex("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", QRegularExpression::CaseInsensitiveOption);
                QRegularExpressionMatch match = uuidRegex.match(fileIdPath);
                if (match.hasMatch()) {
                    // fileIdPath是uuid，从数据库查询文件信息
#ifdef KB_SUPPORT_ENABLED
                    KbDocumentItem item = m_knowledgeBaseDateBase->getItemByUuid(fileIdPath);
#else
                    // 知识库不支持，使用空结构
                    struct { QString documentPath; } item = {""};
#endif
                    if (!item.documentTargetPath.isEmpty()) {
                        documentPath = item.documentTargetPath;
                        QFileInfo fileInfo(item.documentTargetPath);
                        title = fileInfo.fileName();
                        type = fileInfo.suffix().toLower();
                    } else {
                        // 如果查询不到，使用uuid作为title
                        title = fileIdPath;
                        type = "";
                    }
                } else {
                    // fileIdPath是文件路径，直接解析
                    documentPath = fileIdPath;  //保存实际文件路径
                    QFileInfo fileInfo(fileIdPath);
                    title = fileInfo.fileName();
                    type = fileInfo.suffix().toLower();
                }
            }

            QJsonObject itemObj;
            // 为兼容旧的信息来源区域，保留 text 字段


            if (type.isEmpty()) {
                itemObj["text"] = title;
            } else {
                itemObj["text"] = QString("%1 (%2)").arg(title, type);
            }
            // 新增丰富字段供展开区域使用
            itemObj["title"] = title;
            itemObj["type"] = type;
            itemObj["fileid"] = fileIdPath; // fileid保持为uuid或原始值
            itemObj["documentPath"] = documentPath; // 实际文件路径，用于打开文件
            itemObj["content"] = content; // 后端描述内容，用于tooltip显示
            itemObj["description"] = content;
            // 系统指南场景下，补充 tool_id 和 app_name，供前端点击时使用
            if (isSystemGuideChat && infoAppId != -1) {
                itemObj["tool_id"] = infoAppId;
                if (!appName.isEmpty()) {
                    itemObj["app_name"] = appName;
                }
            }
            // 添加调试信息
            qDebug() << "MsgPane: Setting itemObj - fileid:" << fileIdPath << "content:" << content;
            qDebug() << "MsgPane: title:" << title << "type:" << type;
            jsonArray.append(itemObj);
        }
        // 如果过滤后信息来源数组为空，则不发送信息来源（不显示信息来源按钮）
        if (jsonArray.size() > 0) {
            jsonObj["infoSources"] = jsonArray;

            QJsonDocument doc(jsonObj);
            QString jsonData = doc.toJson(QJsonDocument::Indented);
            pushMsg("addRecvSources", jsonData, "");
        }
    }
    m_is_reasoning_end = msg->m_isReasoningEnd;

    // 对话回答带html标签，需先转义，已知<score>替换掉
    recv_message.replace("<score>","");
    recv_message.replace("</score>","");
    recv_message.replace("&lt;score&gt;","");
    recv_message.replace("&lt;/score&gt;","");
    recv_message.replace("<","&lt;");
    recv_message.replace(">","&gt;");

    updateBubble();
}
#else
void CMsgPane::onKBRecvMsg(void *msg, int errorCode)
{
    // 知识库不支持，此函数不应被调用
    Q_UNUSED(msg);
    Q_UNUSED(errorCode);
    KyInfo() << "onKBRecvMsg called but KB_SUPPORT_ENABLED is not defined";
}
#endif


void CMsgPane::onSaveHTML2PDF(const QString &data)
{
    QString htmlContent = data;
    QTextDocument document;
    document.setHtml(htmlContent);

    QString dirPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
    //m_dirPath = picPath + "/" + tr("Docs Processing");
    QString m_dirPath = dirPath + "/kylin-aiassistant/周报";
    QDir dir;
    if(!dir.exists(m_dirPath))
    {
        if(!dir.mkpath(m_dirPath))
        {
            KyInfo() << "创建文档处理目录失败";
        }
        else
        {
            KyInfo() << "创建文档处理目录成功 dirpath = " << m_dirPath;
        }
    }

    //生成pdf文件名
    QDateTime currentTime = QDateTime::currentDateTime();
    QString formattedTime = currentTime.toString("yyyyMMdd_hhmmss");
    // 使用格式化后的时间作为文件名
    QString fileName = "周报_" + formattedTime + ".docx";
    QString path = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).at(0);
    if(path.endsWith("/"))
    {
        path += "kylin-aiassistant/周报/" + fileName;
    }
    else
    {
        path += "/kylin-aiassistant/周报/" + fileName;
    }
    // 使用 QTextDocumentWriter 保存为 HTML 格式
    QString tmpPath = path;
    tmpPath.replace(".docx", ".html");
    QTextDocumentWriter writer(tmpPath);
    writer.setFormat("html"); // 保存为 HTML 格式
    if (!writer.write(&document)) {
        KyInfo() << "Failed to save document.";
    } else {
        KyInfo() << "Document saved as HTML:" << tmpPath;
    }

    // 使用外部工具将 html 转换为 DOCX
    QProcess process;
    QStringList arguments;
#ifdef BUILD_KAIMING
    arguments << tmpPath << "-o" << path << "--reference-doc=/share/kylin-aiassistant/custom-reference.docx";
#else
    arguments << tmpPath << "-o" << path << "--reference-doc=/usr/share/kylin-aiassistant/custom-reference.docx";
#endif
    process.start("pandoc", arguments);
    process.waitForFinished();

    // 删除临时 HTML 文件
    QFile::remove(tmpPath);

    pushMsg("addReportPath", path);
    return;
}

void CMsgPane::onSendDataCollect(const QString &eventCode, const QString &pageName)
{
    //埋点数据发送与处理
    // 初始化埋点数据，传入应用类型，桌面应用，时间类型，点击事件
    KTrackData *node = kdk_dia_data_init(KEVENTSOURCE_DESKTOP, KEVENT_CLICK);
    kdk_dia_upload_default(node, eventCode.toLocal8Bit().data(), pageName.toLocal8Bit().data());
    // 释放内存
    kdk_dia_data_free(node);
}

void CMsgPane::onSendDataCollectWithProps(const QString &eventCode, const QString &pageName, const QVariantMap &customProps)
{
    KTrackData *node = kdk_dia_data_init(KEVENTSOURCE_DESKTOP, KEVENT_CLICK);

    // 构建自定义属性数组
    int propCount = customProps.size();
    QList<QByteArray> keyBufs, valBufs;
    QVector<KCustomProperty> props(propCount);
    int i = 0;
    for (auto it = customProps.cbegin(); it != customProps.cend(); ++it, ++i) {
        keyBufs.append(it.key().toLocal8Bit());
        valBufs.append(it.value().toString().toLocal8Bit());
        props[i].key   = keyBufs.last().data();
        props[i].value = valBufs.last().data();
    }

    if (propCount > 0) {
        kdk_dia_append_custom_property(node, props.data(), propCount);
    }

    kdk_dia_upload_default(node, eventCode.toLocal8Bit().data(), pageName.toLocal8Bit().data());
    kdk_dia_data_free(node);
}

void CMsgPane::onSelectFile(int typeNum)
{
    Q_EMIT sigOpenFile(typeNum);
}

void CMsgPane::onShowImage(const QString &imagePath, int counts)
{
    const auto pushImageValidationTip = [this](const QString &tipMessage) {
        if (m_isReplying) {
            onStopReply();
        }
        QTimer::singleShot(100, this, [this, tipMessage]() {
            QJsonObject jsonObj;
            jsonObj["msgHead"] = msgHead;
            jsonObj["promptId"] = -1;
            jsonObj["promptIconPath"] = "";
            jsonObj["modelMsg"] = "ai";
            jsonObj["colorMsg"] = "red";
            jsonObj["imageSrc"] = "";
            QJsonDocument modelJsonSendWebDoc(jsonObj);
            const QString jsonMsgHeadToWeb = modelJsonSendWebDoc.toJson(QJsonDocument::Indented);
            pushMsg("addRecvMsg", jsonMsgHeadToWeb, tipMessage);
            pushMsg("replyEnd", true);
            Q_EMIT moveScrollBar();
            clearJsonMessage();
        });
    };

    if (counts > 1) {
        pushImageValidationTip(tr("The number of files uploaded cannot exceed 1."));
        return;
    }
    QFile file(imagePath);
    if (!file.open(QIODevice::ReadOnly)) {
        return;
    }
    // 判断图片的大小是否超过20MB
    if (file.size() > 20 * 1024 * 1024) {
        file.close();
        pushImageValidationTip(tr("The size of the uploaded image cannot exceed 20MB."));
        return;
    }
    QString base64Image = getBase64FromImagePath(imagePath);
    QString str = QString("data:image/png;base64," + base64Image);
    Q_EMIT addImagePreview(imagePath, str);
}

QString CMsgPane::onPictureOperateClick(int toolId, const QString &imagePathTem)
{
    m_operatorId = toolId;
    if(imagePathTem.length() == 0)
    {
        KyInfo() << "error imagePath 为空";
        clearJsonMessage();
        jsonObjToWeb["modelMsg"] = "ai";
        jsonObjToWeb["colorMsg"] = "red";
        QJsonDocument aiJsonSendWebDoc(jsonObjToWeb);
        QString jsonMsgHeadToWeb = aiJsonSendWebDoc.toJson(QJsonDocument::Indented);
        pushMsg("addRecvMsg", jsonMsgHeadToWeb, tr("Image processing failed"));
        pushMsg("replyEnd", true);
        clearJsonMessage();
        QJsonObject retJsonObject;
        retJsonObject["isReplying"] = false;
        QJsonDocument retJsonDoc(retJsonObject);
        QString retJsonString = retJsonDoc.toJson(QJsonDocument::Indented);
        return retJsonString;
    }
    else
    {
        QJsonObject jsonObj;
        // 图片分析ID为3，不需要等待动画
        if (toolId != 3){
            jsonObj["drawMsg"] = true;
        }
        jsonObj["operatorId"] = toolId;
        QJsonDocument jDoc(jsonObj);
        QString jsonStr = jDoc.toJson(QJsonDocument::Indented);
        pushMsg("addRecvMsg", jsonStr, tr("Answering"));
        clearJsonMessage();
        imagePath = imagePathTem;
        m_chat->sendToolMessage(toolId, IMAGE, imagePathTem);
        QJsonObject retJsonObject;
        retJsonObject["isReplying"] = true;
        QJsonDocument retJsonDoc(retJsonObject);
        QString retJsonString = retJsonDoc.toJson(QJsonDocument::Indented);
        return retJsonString;
    }
}

void CMsgPane::onImageDragIn(bool dragin)
{
    QString js_str = QString("imageDragIn('%1')").arg(dragin);
//    m_pViewChat->page()->runJavaScript(js_str);
}

void CMsgPane::onShowErrorMoreImage()
{
    QJsonObject jsonObj;
    jsonObj["msgHead"] = msgHead;
    jsonObj["promptId"] = -1;
    jsonObj["promptIconPath"] = "";
    jsonObj["modelMsg"] = "ai";
    jsonObj["colorMsg"] = "red";
    jsonObj["imageSrc"] = "";
    QJsonDocument ModelJsonSendWebDoc(jsonObj);
    QString errorJsonMsgHeadToWeb = ModelJsonSendWebDoc.toJson(QJsonDocument::Indented);
    pushMsg("addRecvMsg", errorJsonMsgHeadToWeb, tr("The number of files uploaded cannot exceed 1."));
    pushMsg("replyEnd", true);
    clearJsonMessage();
}

void CMsgPane::onShowErrorSizeImage()
{
    QJsonObject jsonObj;
    jsonObj["msgHead"] = msgHead;
    jsonObj["promptId"] = -1;
    jsonObj["promptIconPath"] = "";
    jsonObj["modelMsg"] = "ai";
    jsonObj["colorMsg"] = "red";
    jsonObj["imageSrc"] = "";
    QJsonDocument ModelJsonSendWebDoc(jsonObj);
    QString errorJsonMsgHeadToWeb = ModelJsonSendWebDoc.toJson(QJsonDocument::Indented);
    pushMsg("addRecvMsg", errorJsonMsgHeadToWeb, tr("The size of the uploaded file cannot exceed 50MB."));
    pushMsg("replyEnd", true);
    clearJsonMessage();
}

void CMsgPane::onShowErrorFormatImage()
{
    QJsonObject jsonObj;
    jsonObj["msgHead"] = msgHead;
    jsonObj["promptId"] = -1;
    jsonObj["promptIconPath"] = "";
    jsonObj["modelMsg"] = "ai";
    jsonObj["colorMsg"] = "red";
    jsonObj["imageSrc"] = "";
    QJsonDocument ModelJsonSendWebDoc(jsonObj);
    QString errorJsonMsgHeadToWeb = ModelJsonSendWebDoc.toJson(QJsonDocument::Indented);
    pushMsg("addRecvMsg", errorJsonMsgHeadToWeb, tr("The file format is not supported. Please use the following supported formats: "
                                                    "BMP,JPE,JPEG,JPG,PBM,PGM,PNG,PPM,TIFF,WEBP"));
    pushMsg("replyEnd", true);
    clearJsonMessage();
}

QString iconToBase64(const QIcon &icon)
{
    // 将QIcon转换为QPixmap（使用默认大小或指定大小）
    QPixmap pixmap = icon.pixmap(32, 32); // 例如，32x32像素大小
    QByteArray ba;
    QBuffer buffer(&ba);
    buffer.open(QIODevice::WriteOnly);

    // 将QPixmap保存为字节数组
    pixmap.save(&buffer, "PNG"); // 使用PNG格式保存
    buffer.close();

    // 对字节数组进行Base64编码
    QString base64 = ba.toBase64();

    return base64;
}
void CMsgPane::onShowDocument(const QString &filePath)
{
    const QString absolutePath = toAbsoluteLocalFilePath(filePath);
    //传入文件路径，文件图标，文件名，文件大小
    QFileInfo fileInfo(absolutePath);
    if(!fileInfo.exists())
    {
        KyInfo() << "文件不存在:" << absolutePath;
        return;
    }

    QString fileName = fileInfo.fileName();

    // 获取图标 - 使用与知识库管理一致的图标路径
    QString str = Globals::getFileIconPath(absolutePath);

    qint64 size = fileInfo.size();
    double fileSizeKB = static_cast<double>(size) / 1024;
    // 上传文件大小不能超过50MB
    if(fileSizeKB > 50 * 1024){
        if (m_isReplying) {
            onStopReply();
        }
        QTimer::singleShot(100, this, [=]{
            QJsonObject jsonObj;
            jsonObj["msgHead"] = msgHead;
            jsonObj["promptId"] = -1;
            jsonObj["promptIconPath"] = "";
            jsonObj["modelMsg"] = "ai";
            jsonObj["colorMsg"] = "red";
            jsonObj["imageSrc"] = "";
            QJsonDocument ModelJsonSendWebDoc(jsonObj);
            QString errorJsonMsgHeadToWeb = ModelJsonSendWebDoc.toJson(QJsonDocument::Indented);
            pushMsg("addRecvMsg", errorJsonMsgHeadToWeb, tr("The size of the uploaded file cannot exceed 50MB."));
            pushMsg("replyEnd", true);
            clearJsonMessage();
        });
        return;
    }
    QString fileSize;
    if(fileSizeKB > 1024)
    {
        double fileSizeMB = static_cast<double>(fileSizeKB) / 1024;
        fileSize = QString::number(fileSizeMB, 'f', 2) + "MB";
    }
    else
    {
        fileSize = QString::number(fileSizeKB, 'f', 2) + "KB";
    }
    activateMainWindow();
    Q_EMIT addDocumentPreview(absolutePath, str, fileName, fileSize);
}

void CMsgPane::onShowErrorFormatFile()
{
    QJsonObject jsonObj;
    jsonObj["msgHead"] = msgHead;
    jsonObj["promptId"] = -1;
    jsonObj["promptIconPath"] = "";
    jsonObj["modelMsg"] = "ai";
    jsonObj["colorMsg"] = "red";
    jsonObj["imageSrc"] = "";
    QJsonDocument ModelJsonSendWebDoc(jsonObj);
    QString errorJsonMsgHeadToWeb = ModelJsonSendWebDoc.toJson(QJsonDocument::Indented);
    pushMsg("addRecvMsg", errorJsonMsgHeadToWeb, tr("The file format is not supported. Please use the following supported formats: "
                                                 "txt、pdf、docx、pptx"));
    pushMsg("replyEnd", true);
    clearJsonMessage();
}

void CMsgPane::onShowErrorFormat()
{
    QJsonObject jsonObj;
    jsonObj["msgHead"] = msgHead;
    jsonObj["promptId"] = -1;
    jsonObj["promptIconPath"] = "";
    jsonObj["modelMsg"] = "ai";
    jsonObj["colorMsg"] = "red";
    jsonObj["imageSrc"] = "";
    QJsonDocument ModelJsonSendWebDoc(jsonObj);
    QString errorJsonMsgHeadToWeb = ModelJsonSendWebDoc.toJson(QJsonDocument::Indented);
    pushMsg("addRecvMsg", errorJsonMsgHeadToWeb, tr("The file format is not supported. Please use the following supported formats:\n"
                                                 "Images: bmp、jpe、jpeg、jpg、pbm、pgm、png、ppm、tiff、webp\n"
                                                 "Documents: txt、pdf、docx、pptx"));
    pushMsg("replyEnd", true);
    clearJsonMessage();
}

QString CMsgPane::getBase64FromImagePath(const QString &imagePath)
{
    QFile file(imagePath);
    if(!file.open(QIODevice::ReadOnly))
    {
        // 错误处理：文件无法打开
        return QString();
    }

    QByteArray fileData = file.readAll();
    file.close();

    return fileData.toBase64();
}

void CMsgPane::callSourceUrlTool(int infoAppId)
{
    // 根据工具 id 反查 appName
    const QString appName = getAppNameFromSourceId(infoAppId);
    if (appName.isEmpty()) {
        KyInfo() << "callSourceUrlTool: appName is empty, infoAppId =" << infoAppId;
        return;
    }

    QStringList arguments;
    arguments << "-A" << appName;

    KyInfo() << "callSourceUrlTool: kylin-user-guide" << arguments;
    QProcess::startDetached("kylin-user-guide", arguments);
    return;
}

void CMsgPane::errorMessageToJs(QString msgHead, QString modelMsg, QString colorMsg, QString errorMsg)
{
    QJsonObject jsonObj;
    jsonObj["msgHead"] = msgHead;
    jsonObj["promptId"] = -1;
    jsonObj["promptIconPath"] = "";
    jsonObj["modelMsg"] = modelMsg;
    jsonObj["colorMsg"] = colorMsg;
    jsonObj["imageSrc"] = "";
    jsonObj["drawMsg"] = false;
    QJsonDocument ModelJsonSendWebDoc(jsonObj);
    QString errorJsonMsgHeadToWeb = ModelJsonSendWebDoc.toJson(QJsonDocument::Indented);
    pushMsg("updateMsg", errorJsonMsgHeadToWeb, errorMsg);
    QTimer *tempTimer = new QTimer();
    connect(tempTimer,&QTimer::timeout,[&]{
        pushMsg("replyEnd", true);
    });
    tempTimer->setSingleShot(true);
    tempTimer->start(50);
    Q_EMIT moveScrollBar();
}

void CMsgPane::errorMessageToJsNoModel(QString msgHead, QString modelMsg, QString colorMsg, QString errorMsg)
{
    // 临时用于2503U1的UI显示，2507可删除
    QJsonObject jsonObj;
    jsonObj["msgHead"] = msgHead;
    jsonObj["promptId"] = -1;
    jsonObj["promptIconPath"] = "";
    jsonObj["modelMsg"] = modelMsg;
    jsonObj["colorMsg"] = colorMsg;
    jsonObj["imageSrc"] = "";
    jsonObj["drawMsg"] = false;
    jsonObj["noModelError"] = "true";
    QJsonDocument ModelJsonSendWebDoc(jsonObj);
    QString errorJsonMsgHeadToWeb = ModelJsonSendWebDoc.toJson(QJsonDocument::Indented);
    pushMsg("updateMsg", errorJsonMsgHeadToWeb, errorMsg);
    pushMsg("replyEnd", true);
    Q_EMIT moveScrollBar();
}

void CMsgPane::systemMessageToJs(QString msgHead, QString modelMsg, QString colorMsg, QString systemName, QString systemIcon, QString msg, int systemIndex, CMsgPane::ChatType chatType)
{
    //向前端发送一条新的系统控制的气泡
    QJsonObject jsonObj;
    jsonObj["msgHead"] = msgHead;
    jsonObj["promptId"] = -1;
    jsonObj["promptIconPath"] = "";
    jsonObj["modelMsg"] = modelMsg;
    jsonObj["imageSrc"] = "";
    jsonObj["colorMsg"] = colorMsg;
    jsonObj["drawMsg"] = false;
    jsonObj["systemIcon"] = systemIcon;
    jsonObj["systemName"] = systemName;
    jsonObj["systemMsg"] = msg;
    jsonObj["systemIndex"] = systemIndex;
    jsonObj["chatType"] = chatType;
    QJsonDocument ModelJsonSendWebDoc(jsonObj);
    QString errorJsonMsgHeadToWeb = ModelJsonSendWebDoc.toJson(QJsonDocument::Indented);
    pushMsg("addRecvMsg", errorJsonMsgHeadToWeb, "");
    pushMsg("replyEnd", true);
    Q_EMIT moveScrollBar();
}

void CMsgPane::errorMessageToKbJs(QString msgHead, QString modelMsg, QString colorMsg, QString errorMsg, CMsgPane::ChatType chatType, QString isModelError)
{
    QJsonObject jsonObj;
    jsonObj["msgHead"] = msgHead;
    jsonObj["promptId"] = -1;
    jsonObj["promptIconPath"] = "";
    jsonObj["modelMsg"] = modelMsg;
    jsonObj["colorMsg"] = colorMsg;
    jsonObj["imageSrc"] = "";
    jsonObj["drawMsg"] = false;
    jsonObj["chatType"] = chatType;
    jsonObj["noModelError"] = isModelError;
    QJsonDocument ModelJsonSendWebDoc(jsonObj);

    QString errorJsonMsgHeadToWeb = ModelJsonSendWebDoc.toJson(QJsonDocument::Indented);
    pushMsg("updateMsg", errorJsonMsgHeadToWeb, errorMsg);
    pushMsg("replyEnd", true);

    Q_EMIT moveScrollBar();
}

void CMsgPane::clearJsonMessage()
{
    jsonObjToWeb["msgHead"] = "";
    jsonObjToWeb["promptId"] = -1;
    jsonObjToWeb["promptIconPath"] = "";
    jsonObjToWeb["modelMsg"] = "";
    jsonObjToWeb["colorMsg"] = "";
    jsonObjToWeb["drawMsg"] = false;
    jsonObjToWeb["imageSrc"] = "";
    jsonObjToWeb["speakerId"] = "";
    jsonObjToWeb["imagePath"] = "";
    jsonObjToWeb["imageSrc"] = "";
    jsonObjToWeb["year"] = -1;
    jsonObjToWeb["month"] = -1;
    jsonObjToWeb["day"] = -1;
    jsonObjToWeb["isAddSchedule"] = false;
    jsonObjToWeb["allFollowUpQuestion"] = "";
}

void CMsgPane::updateToShowSource(QMap<int, QString> infoSourceMap)
{
    QJsonObject jsonObj;
    QJsonArray jsonArray;
    for(auto i = infoSourceMap.constBegin(); i != infoSourceMap.constEnd(); ++i)
    {
        QJsonObject toolObj;
        toolObj["tool_id"] = i.key();
        toolObj["text"] = i.value();
        jsonArray.append(toolObj);
    }
    jsonObj["infoSources"] = jsonArray;

    QJsonDocument doc(jsonObj);
    QString jsonData = doc.toJson(QJsonDocument::Indented);

    pushMsg("addRecvSources", jsonData, "");
}

void CMsgPane::updateToShowWebSearchSource(QJsonArray searchSourcesArray)
{
    QJsonObject jsonObj;
    // 使用独立的字段名 webSearchSources，区别于 information_sources 的 infoSources
    // 前端可以根据字段名区分：infoSources（模型引用的信息）vs webSearchSources（模型查询的网页）
    jsonObj["webSearchSources"] = searchSourcesArray;

    QJsonDocument doc(jsonObj);
    QString jsonData = doc.toJson(QJsonDocument::Indented);

    // 复用 addRecvSources 消息类型，但使用不同的字段名
    pushMsg("addRecvSources", jsonData, "");
}

void CMsgPane::onSelectInfoSource(int toolId)
{
    KyInfo() << "__________________"
             << "toolId" << toolId;
    m_chat->sendToolMessage(toolId, SOURCE, "");
//    QString js_str2 = QString("setReplyStale()"); //设置上一条回答不可重新生成
//    m_pViewChat->page()->runJavaScript(js_str2);
}

void CMsgPane::initPluginList()
{
    // 语音播报
//    QString plugin2 = QString(R"({
//        "image":"qrc:/icon/icon/sidebar/voice_broadcast.svg",
//        "pluginName":"%1",
//        "pluginDescription":"%2",
//        "qmlPath":"qrc:/qml/qml/Plugin2.qml"
//    })").arg(tr("语音播报")).arg(tr("图片内文本转成语音"));
//    m_pluginList << plugin2;

    // 语音听写
//    QString plugin3 = QString(R"({
//        "image":"qrc:/icon/icon/sidebar/voice_dictation.svg",
//        "pluginName":"%1",
//        "pluginDescription":"%2",
//        "qmlPath":"qrc:/qml/qml/Plugin3.qml"
//    })").arg(tr("语音听写")).arg(tr("实时将人声转换成文字"));
//    m_pluginList << plugin3;
    return;
}

void CMsgPane::initCommandList()
{
    m_commandListModel = CommandTemplateList::getInstance();
    CommandTemplateList::originalCommandListModel = m_commandListModel;

    return;
}

void CMsgPane::initLoadingTimer()
{
    m_loadTimer = new QTimer;
    connect(m_loadTimer,&QTimer::timeout,this,[&]{
        KyInfo() << "[ChatSDK] UI init stage: 30s wait timer expired; emitting chatInitCompleted (SDK may not have succeeded — check prior [ChatSDK] init logs)";
        Q_EMIT chatInitCompleted();
    });
    m_loadTimer->setSingleShot(true);
    m_loadTimer->start(30000);
}

void CMsgPane::openDocument(const QString &documentPath)
{
    //KyInfo() << "documentPath" << documentPath;
    // 检查文件和文件夹是否存在
    if(!QFileInfo::exists(documentPath))
    {
        Q_EMIT sendOpenDocumentMessage("qrc:/icon/icon/dialog-error.svg", tr("No such file or directory!"));
        return;
    }

    QString path = QString("file://") + documentPath;
    //KyInfo() << "path" << path;
    bool is_open = QDesktopServices::openUrl(QUrl(path, QUrl::TolerantMode));

    if(!is_open)
    {
        //KyInfo() << "open by xdg";
        QString cmd = QString("xdg-open ") + path;
        QProcess::startDetached(cmd);
    }
}

bool CMsgPane::isSupportDocumentQA()
{
    return m_chat->getFeatureStatus(Features::Feature::DocumentQA);
}

bool CMsgPane::isSupportHtml2Docx()
{
    QString path = "/usr/bin/pandoc";
    if (QFileInfo::exists(path)) {
        KyInfo() << "/usr/bin/pandoc-文件存在";
        return true;
    } else {
        KyInfo() << "/usr/bin/pandoc-文件不存在";
    }
    return false;
}

bool CMsgPane::isSupportImageProcessing()
{
    return false;
//    return m_chat->getFeatureStatus(Features::Feature::ImageProcessing);
}

int CMsgPane::getCurrentSessionId()
{
    return m_chat->getSession();
}

void CMsgPane::onMeetRuningVoiceUnableError()
{
    QJsonObject jsonObjToWeb;
    jsonObjToWeb["modelMsg"] = "ai";
    jsonObjToWeb["colorMsg"] = "red";
    QJsonDocument aiJsonSendWebDoc(jsonObjToWeb);
    QString jsonMsgHeadToWeb = aiJsonSendWebDoc.toJson(QJsonDocument::Indented);
    pushMsg("addRecvMsg", jsonMsgHeadToWeb, tr("The microphone is occupied and voice conversations cannot be used. Please close other apps and try again."));
    pushMsg("replyEnd", true);
}

void CMsgPane::activateMainWindow()
{
    // 发射信号
    Q_EMIT activateMainWindowSignal();
}

void CMsgPane::updateDialogueListSessionId()
{
    Q_EMIT updateDialogueList();
}

void CMsgPane::onStopSpeak()
{
    Q_EMIT cancelVoiceStopReplying();
}

void CMsgPane::stopVoiceInputIfActive()
{
    pushMsg("stopVoiceInputIfActive");
}

void CMsgPane::openUkuiControlCenter()
{
    QProcess process;
    QString command = "ukui-control-center";
    QStringList arguments;
    arguments << "-m" << "aisubsystem";
    process.startDetached(command, arguments);
    process.waitForFinished(-1);
    // 检查进程是否正常退出
    if (process.exitStatus() == QProcess::NormalExit) {
        KyInfo() << "ukui-control-center successfully!";
    } else {
        KyInfo() << "Failed  to open ukui-control-center";
    }
}

void CMsgPane::openUkuiUpgrade()
{
    QProcess process;
    QString command = "ukui-control-center";
    QStringList arguments;
    arguments << "-m" << "upgrade";
    process.startDetached(command, arguments);
    process.waitForFinished(-1);
    if (process.exitStatus() == QProcess::NormalExit) {
        KyInfo() << "ukui-control-center upgrade page opened successfully!";
    } else {
        KyInfo() << "Failed to open ukui-control-center upgrade page";
    }
}

void CMsgPane::openSoftwareCenter()
{
    QString command = "kylin-software-center";
    QStringList arguments;
    arguments << "-find" << "kylin-aiassistant";

    // startDetached 返回是否成功启动进程
    qint64 pid = 0;
    bool success = QProcess::startDetached(command, arguments, QString(), &pid);

    if (success && pid > 0) {
        KyInfo() << "kylin-software-center successfully! PID:" << pid;
    } else {
        KyInfo() << "Failed to open kylin-software-center. Command:" << command << "Arguments:" << arguments;
    }
}

void CMsgPane::setWindowAlwaysOnTop(bool checked)
{
    // 调用CMainWnd的方法设置窗口置顶
    CMainWnd *mainWnd = CMainWnd::getInstance();
    if (mainWnd) {
        mainWnd->setWindowAlwaysOnTop(checked);
    }
}

void CMsgPane::showHelp()
{
    // 调用CMainWnd的方法显示帮助手册
    CMainWnd *mainWnd = CMainWnd::getInstance();
    if (mainWnd) {
        mainWnd->onShowHelp();
    }
}

void CMsgPane::showFeedBack()
{
    QString directoryPath = "/usr/lib/kylin-os-manager/bin/service-support-mini";
    QFile file(directoryPath);
    if (file.exists()) {
        QStringList arguments;
        QString processName;
        QString fileName = QString("/proc/%1/cmdline").arg(QApplication::applicationPid());
        QFile file(fileName);
        if (file.open(QIODevice::ReadOnly)) {
            QByteArray data = file.readAll();
            int pos = data.indexOf('\0');
            if (pos != -1) {
                QStringList list = QString::fromUtf8(data.left(pos)).trimmed().split("/");
                processName = list.last();
            }
            file.close();
        }

        arguments << "-mode=1"
                  << "-exe=" + processName;

        QProcess m_feedbackProcess;
        m_feedbackProcess.startDetached(directoryPath, arguments);
        m_feedbackProcess.waitForFinished();
    }
}

void CMsgPane::showAbout()
{
    // 调用CMainWnd的方法显示关于窗口
    CMainWnd *mainWnd = CMainWnd::getInstance();
    if (mainWnd) {
        mainWnd->onShowAbout();
    }
}

void CMsgPane::startVoiceChat()
{
#if defined(BUILD_KAIMING)
    const QString program = QStringLiteral("/bin/voice-assistant");
#else
    const QString program = QStringLiteral("/usr/bin/voice-assistant");
#endif
    if (!QFileInfo::exists(program)) {
        NotificationNotify::getInstance()->sendNotification(
            tr("Voice assistant"), tr("Voice assistant is not installed."), QString(), false);
        return;
    }
    const QString workDir = QFileInfo(program).absolutePath();
    qint64 pid = 0;
    if (!QProcess::startDetached(program, QStringList(), workDir, &pid)) {
        KyInfo() << "startVoiceChat: startDetached failed:" << program;
    }
}

void CMsgPane::stopVoiceChat()
{
    // 通过 D-Bus 通知 voice-assistant 自行退出，之后再关闭主窗口，防止交叉清理卡死
    QDBusConnectionInterface *iface = QDBusConnection::sessionBus().interface();
    if (!iface || !iface->isServiceRegistered(QStringLiteral("org.ukui.voice_assistant"))) {
        return;
    }

    QDBusInterface vaIface(QStringLiteral("org.ukui.voice_assistant"),
                           QStringLiteral("/org/ukui/voice_assistant"),
                           QStringLiteral("org.ukui.voice_assistant"),
                           QDBusConnection::sessionBus());
    if (vaIface.isValid()) {
        vaIface.asyncCall(QStringLiteral("Quit"));
    }
}

void CMsgPane::onStartReplyingToChat()
{
    Q_EMIT startReplyingToChat(true);
}

void CMsgPane::saveToDatabase(int msgInex, QString message)
{
    QString addressAsString;
    if (m_chatType == TASK_MODE) {
        TaskSession* activeSession = m_taskSessionManager->getSession(m_taskSessionManager->getActiveSessionUuid());
        int historyId = activeSession ? activeSession->historyId : 0;
        QString activeUuid = m_taskSessionManager->getActiveSessionUuid();
        if (!activeUuid.isEmpty()) {
            addressAsString = QString::number(historyId);
            m_database->insertOneWithModeTypeAndUuid(addressAsString, msgInex, message, 0, 1, activeUuid);
            m_database->updateOperateTime(addressAsString, msgInex);
            int maxHistoryId = getCurrentMaxHistoryId();
            if(historyId == maxHistoryId)
            {
                m_database->updateMaxHistoryId(maxHistoryId + 1);
            }
        }
    } else {
        int curID = m_chat->getSession();
        addressAsString = QString::number(curID);
        m_database->insertOne(addressAsString, msgInex, message, 0);
        m_database->updateOperateTime(addressAsString, msgInex);

        int maxHistoryId = getCurrentMaxHistoryId();
        if(curID == maxHistoryId)
        {
            m_database->updateMaxHistoryId(maxHistoryId + 1);
        }
    }

    // 从消息 JSON 中提取文件信息并保存到文件引用表
    QJsonDocument jsonDoc = QJsonDocument::fromJson(message.toUtf8());
    if (jsonDoc.isObject()) {
        QJsonObject jsonObj = jsonDoc.object();
        QString author = jsonObj.value("author").toString();

        // 只处理用户消息
        if (author == "User") {
            // 检查是否是生成待办消息，如果是则不保存文件到文件引用数据库
            bool isGenerateToDo = jsonObj.value("isGenerateToDo").toBool(false);
            if (!isGenerateToDo) {
                QString fileType = jsonObj.value("fileType").toString();
                QString documents = jsonObj.value("documents").toString();

                // 如果有文件信息，保存到文件引用表
                if (!fileType.isEmpty() && !documents.isEmpty()) {
                    DocumentReferenceDatabase::getInstance()->insertDocumentReference(addressAsString, fileType, documents);
                }
            }
        }
    }

    int curWinMode = getCurWinMode();
    //窗口模式
    if(curWinMode == 3){
         getDialogueList();
    }

}

void CMsgPane::updateLastMessage(int msgInex, QString message)
{
    int curID = m_chat->getSession();
    QString addressAsString = QString::number(curID);
    m_database->updateFeedback(addressAsString, msgInex, message);
}

void CMsgPane::updateFeedbackDatabase(int msgInex, QString message)
{
    int curID = m_chat->getSession();
    QString addressAsString = QString::number(curID);
    m_database->updateFeedback(addressAsString, msgInex, message);
}

void CMsgPane::deleteMessageByIndex(int msgIndex)
{
    int curID = m_chat->getSession();
    QString addressAsString = QString::number(curID);
    m_database->deleteBySessionIDAndIndex(addressAsString, msgIndex);
    // 注意：文件引用表不存储 msgIndex，删除消息时不需要删除文件引用
    // 文件引用是按会话存储的，只有在删除整个会话或更新文件引用时才需要操作
    Q_EMIT sigDeleteMessageAt(msgIndex);
}

void CMsgPane::clearCurrentSessionDocumentReference()
{
    int curID = m_chat->getSession();
    QString addressAsString = QString::number(curID);
    DocumentReferenceDatabase::getInstance()->deleteDocumentReferenceBySessionID(addressAsString);
}

void CMsgPane::requestDeleteMessage(int msgIndex)
{
    Q_EMIT sigShowDeleteMessageBox(msgIndex);
}

void CMsgPane::updateIsCollectBySessionId(QString sessionId, int isCollect)
{
    m_database->updateIsCollect(sessionId, isCollect);
}

void CMsgPane::updateIsCollectBySessionIdList(const QVariantList &sessionIds, int isCollect)
{
    for (const QVariant &sessionId : sessionIds) {
        QString sessionIdStr = sessionId.toString();
        m_database->updateIsCollect(sessionIdStr, isCollect);
    }
}

// 与 res/qml/Chat.qml 的 onAddMsg 默认值保持一致；供智能体历史加载与 kybot cron 同步使用。
static void fillKybotChatDefaults(QJsonObject& json)
{
    if (!json.contains("colorMsg"))                      json["colorMsg"] = "";
    if (!json.contains("drawMsg"))                       json["drawMsg"] = false;
    if (!json.contains("imageSrc"))                      json["imageSrc"] = "";
    if (!json.contains("imagePath"))                     json["imagePath"] = "";
    if (!json.contains("showImageOptions"))              json["showImageOptions"] = false;
    if (!json.contains("showRead"))                      json["showRead"] = true;
    if (!json.contains("msgHead"))                       json["msgHead"] = "";
    if (!json.contains("promptId"))                      json["promptId"] = -1;
    if (!json.contains("modelMsg"))                      json["modelMsg"] = "";
    if (!json.contains("isCleared"))                     json["isCleared"] = false;
    if (!json.contains("isAddSchedule"))                 json["isAddSchedule"] = false;
    if (!json.contains("isGenerateToDo"))                json["isGenerateToDo"] = false;
    if (!json.contains("isSkipAddSchedule"))             json["isSkipAddSchedule"] = false;
    if (!json.contains("isAddScheduleShow"))             json["isAddScheduleShow"] = false;
    if (!json.contains("pdfPath"))                       json["pdfPath"] = "";
    if (!json.contains("noModelError"))                  json["noModelError"] = "";
    if (!json.contains("voiceModelError"))               json["voiceModelError"] = false;
    if (!json.contains("isFirst"))                       json["isFirst"] = true;
    if (!json.contains("speechFlag"))                    json["speechFlag"] = false;
    if (!json.contains("isShowInfoSources"))             json["isShowInfoSources"] = false;
    if (!json.contains("infoSources"))                   json["infoSources"] = "";
    if (!json.contains("isShowWebSearchSources"))        json["isShowWebSearchSources"] = false;
    if (!json.contains("webSearchSources"))              json["webSearchSources"] = "";
    if (!json.contains("pictureOperates"))               json["pictureOperates"] = "";
    if (!json.contains("isShowPictureOperateRefresh"))   json["isShowPictureOperateRefresh"] = true;
    if (!json.contains("operatorId"))                    json["operatorId"] = 0;
    if (!json.contains("isLike"))                        json["isLike"] = false;
    if (!json.contains("isDislike"))                     json["isDislike"] = false;
    if (!json.contains("feedbackURL"))                   json["feedbackURL"] = "";
    if (!json.contains("closePopup"))                    json["closePopup"] = false;
    if (!json.contains("promptIconPath"))                json["promptIconPath"] = "";
    if (!json.contains("allFollowUpQuestion"))           json["allFollowUpQuestion"] = "";
    if (!json.contains("documents"))                     json["documents"] = "";
    if (!json.contains("fileType"))                      json["fileType"] = "";
    if (!json.contains("questionModeEnabledWhenAnswer")) json["questionModeEnabledWhenAnswer"] = "";
    if (!json.contains("targetDocument"))                json["targetDocument"] = "";
    if (!json.contains("todoList"))                      json["todoList"] = "";
    if (!json.contains("commandUuid"))                   json["commandUuid"] = "";
    if (!json.contains("taskToolCalls"))                 json["taskToolCalls"] = "";
    if (!json.contains("isRefreshButtonVisiable"))       json["isRefreshButtonVisiable"] = true;
    if (!json.contains("reasonMessage"))                 json["reasonMessage"] = "";

    // QJsonValue::toString() 将 null/缺失统一为空串，避免 QML 中 undefined/null !== "" 误判
    json["colorMsg"] = json.value("colorMsg").toString() == "red" ? "red" : "";
    json["pdfPath"] = json.value("pdfPath").toString();
    json["noModelError"] = json.value("noModelError").toString();
}

//去除字符串里面的标签
QString removeHtmlTags(const QString &input) {
    QTextDocument doc;
    doc.setHtml(input);
    return doc.toPlainText();  // 返回纯文本
}

static QString normalizeVoiceAssistantHistoryMessage(const QString &msgStr)
{
    QJsonParseError jsonErr;
    QJsonDocument jsonDoc = QJsonDocument::fromJson(msgStr.toUtf8(), &jsonErr);
    if (jsonErr.error != QJsonParseError::NoError || !jsonDoc.isObject()) {
        return msgStr;
    }

    QJsonObject obj = jsonDoc.object();
    const bool isVoiceAssistantShape = obj.contains("sentByUser") || obj.contains("uniqueIndex");
    if (!isVoiceAssistantShape) {
        return msgStr;
    }

    const bool sentByUser = obj.value("sentByUser").toBool(false);
    obj["author"] = sentByUser ? "User" : "Bot";
    obj["isEnd"] = obj.value("isEnd").toBool(true);
    obj["isRefreshButtonVisiable"] = sentByUser ? false : obj.value("isRefreshButtonVisiable").toBool(true);
    obj["colorMsg"] = obj.value("colorMsg").toString() == "red" ? "red" : "";
    obj["drawMsg"] = obj.value("drawMsg").toBool(false);
    obj["imageSrc"] = obj.value("imageSrc").toString();
    obj["imagePath"] = obj.value("imagePath").toString();
    obj["showImageOptions"] = obj.value("showImageOptions").toBool(false);
    obj["showRead"] = obj.value("showRead").toBool(true);
    obj["msgHead"] = obj.value("msgHead").toString();
    obj["promptId"] = obj.value("promptId").toInt(-1);
    obj["promptIconPath"] = obj.value("promptIconPath").toString();
    const QString modelMsg = obj.value("modelMsg").toString();
    obj["modelMsg"] = (!sentByUser && modelMsg.isEmpty()) ? modelMsgForTaskMode() : modelMsg;
    obj["reasonMessage"] = obj.value("reasonMessage").toString();
    obj["isCleared"] = obj.value("isCleared").toBool(false);
    obj["isAddSchedule"] = obj.value("isAddSchedule").toBool(false);
    obj["isGenerateToDo"] = obj.value("isGenerateToDo").toBool(false);
    obj["isSkipAddSchedule"] = obj.value("isSkipAddSchedule").toBool(false);
    obj["isAddScheduleShow"] = obj.value("isAddScheduleShow").toBool(false);
    obj["pdfPath"] = obj.value("pdfPath").toString();
    obj["noModelError"] = obj.value("noModelError").toString();
    obj["isFirst"] = obj.value("isFirst").toBool(true);
    obj["speechFlag"] = true;
    obj["modeType"] = 1;
    const QString docs = obj.value(QStringLiteral("documents")).toString();
    obj["documents"] = docs.isEmpty() ? QStringLiteral("[]") : docs;
    obj["fileType"] = obj.value(QStringLiteral("fileType")).toString();
    obj["infoSources"] = obj.value("infoSources").toString();
    obj["isShowWebSearchSources"] = obj.value("isShowWebSearchSources").toBool(false);
    obj["webSearchSources"] = obj.value("webSearchSources").toString();
    obj["pictureOperates"] = obj.value("pictureOperates").toString();
    obj["isShowPictureOperateRefresh"] = obj.value("isShowPictureOperateRefresh").toBool(true);
    obj["operatorId"] = obj.value("operatorId").toInt(0);
    obj["isLike"] = obj.value("isLike").toBool(false);
    obj["isDislike"] = obj.value("isDislike").toBool(false);
    obj["feedbackURL"] = obj.value("feedbackURL").toString();
    obj["closePopup"] = obj.value("closePopup").toBool(false);
    obj["allFollowUpQuestion"] = obj.value("allFollowUpQuestion").toString();
    obj["questionModeEnabledWhenAnswer"] = obj.value("questionModeEnabledWhenAnswer").toString();
    obj["deepThinkingEnabledWhenCreated"] = obj.value("deepThinkingEnabledWhenCreated").toBool(false);
    obj["targetDocument"] = obj.value("targetDocument").toString();
    obj["todoList"] = obj.value("todoList").toString();
    obj["commandUuid"] = obj.value("commandUuid").toString();
    obj["taskToolCalls"] = obj.value("taskToolCalls").toString();

    return QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact));
}

static QStringList normalizeVoiceAssistantHistoryMessages(const QStringList &list)
{
    QStringList normalized;
    normalized.reserve(list.size());
    for (const QString &item : list) {
        normalized.append(normalizeVoiceAssistantHistoryMessage(item));
    }
    return normalized;
}

static QStringList filterVoiceMessagesFromTextHistory(const QStringList &list)
{
    QStringList filtered;
    filtered.reserve(list.size());
    for (const QString &item : list) {
        QJsonParseError jsonErr;
        const QJsonDocument jsonDoc = QJsonDocument::fromJson(item.toUtf8(), &jsonErr);
        if (jsonErr.error == QJsonParseError::NoError && jsonDoc.isObject()
                && isVoiceAssistantStoredMessage(jsonDoc.object())) {
            continue;
        }
        filtered.append(item);
    }
    return filtered;
}

static bool isVoiceBotErrorMessage(const QJsonObject &obj)
{
    const bool sentByUser = obj.value(QStringLiteral("sentByUser")).toBool(false);
    const QString author = obj.value(QStringLiteral("author")).toString();
    const bool isBot = author == QLatin1String("Bot") || (!obj.contains(QStringLiteral("author")) && !sentByUser);
    return isBot
            && obj.value(QStringLiteral("colorMsg")).toString() == QLatin1String("red")
            && !obj.value(QStringLiteral("message")).toString().trimmed().isEmpty();
}

static QStringList dedupeConsecutiveVoiceErrorMessages(const QStringList &list)
{
    QStringList deduped;
    deduped.reserve(list.size());
    QString lastBotErrorKey;
    for (const QString &item : list) {
        QJsonParseError jsonErr;
        const QJsonDocument jsonDoc = QJsonDocument::fromJson(item.toUtf8(), &jsonErr);
        if (jsonErr.error == QJsonParseError::NoError && jsonDoc.isObject()) {
            const QJsonObject obj = jsonDoc.object();
            if (isVoiceBotErrorMessage(obj)) {
                const QString key = obj.value(QStringLiteral("message")).toString().trimmed();
                if (key == lastBotErrorKey) {
                    continue;
                }
                lastBotErrorKey = key;
            } else {
                lastBotErrorKey.clear();
            }
        }
        deduped.append(item);
    }
    return deduped;
}

static std::vector<Message> buildVoiceAssistantChatHistoryFromConversationList(const QStringList &list)
{
    std::vector<Message> history;
    for (const QString &jsonStr : list) {
        QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonStr.toUtf8());
        if (!jsonDoc.isObject()) {
            continue;
        }
        QJsonObject jsonObj = jsonDoc.object();
        QString author = jsonObj.value(QStringLiteral("author")).toString();
        if (author.isEmpty()) {
            author = jsonObj.value(QStringLiteral("sentByUser")).toBool(false)
                    ? QStringLiteral("User") : QStringLiteral("Bot");
        }
        const std::string msg = removeHtmlTags(jsonObj.value(QStringLiteral("message")).toString()).toStdString();
        if (msg.empty()) {
            continue;
        }
        Message m;
        if (author == QLatin1String("User")) {
            m.user = msg;
        } else {
            m.assistant = msg;
        }
        history.push_back(m);
    }
    return history;
}

// 智能体（task mode）历史加载专用：补齐缺失字段，避免 QML 中 undefined 误判 UI 可见性。
static QString normalizeTaskSessionHistoryMessage(const QString &msgStr)
{
    QJsonParseError jsonErr;
    QJsonDocument jsonDoc = QJsonDocument::fromJson(msgStr.toUtf8(), &jsonErr);
    if (jsonErr.error != QJsonParseError::NoError || !jsonDoc.isObject()) {
        return msgStr;
    }

    QJsonObject obj = jsonDoc.object();
    if (obj.contains("sentByUser") || obj.contains("uniqueIndex")) {
        return normalizeVoiceAssistantHistoryMessage(msgStr);
    }

    fillKybotChatDefaults(obj);
    return QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact));
}

static QStringList normalizeTaskSessionHistoryMessages(const QStringList &list)
{
    QStringList normalized;
    normalized.reserve(list.size());
    for (const QString &item : list) {
        normalized.append(normalizeTaskSessionHistoryMessage(item));
    }
    return normalized;
}

void CMsgPane::loadHistory()
{
    QStringList list =  m_database->selectAll();
    Q_EMIT loadConversation(list);
}

static std::vector<Message> buildChatHistoryFromConversationList(const QStringList &list)
{
    std::vector<Message> history;
    bool isSkip = false;  //用于标识文档问答和携带指令的提问，如果遇到此类提问，不拼接下一条回答

    for (const QString &jsonStr : list) {
        if (isSkip) {
            isSkip = false;
            continue;
        }

        QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonStr.toUtf8());
        if (!jsonDoc.isObject()) {
            qWarning() << "Invalid JSON:" << jsonStr;
            continue;
        }

        QJsonObject jsonObj = jsonDoc.object();
        QString author = jsonObj.value("author").toString();
        if (author.isEmpty()) {
            author = jsonObj.value("sentByUser").toBool(false)
                    ? QStringLiteral("User") : QStringLiteral("Bot");
        }
        std::string msg = removeHtmlTags(jsonObj.value("message").toString()).toStdString();
        QString command = jsonObj.value("msgHead").toString();
        QString filePath = jsonObj.value("documentPath").toString();
        if (command != "" || filePath != "") {
            history.clear();
            isSkip = true;
            continue;
        }

        Message m;
        if (author == "User") {
            m.user = msg;
        } else {
            m.assistant = msg;
        }
        history.push_back(m);
    }

    return history;
}

void CMsgPane::loadHistoryBySessionId(QString sessionId)
{
    m_database->setUnreadFlag(sessionId, 0);
    int modeType = m_database->getSessionModeType(sessionId);
    const bool isVoiceAssistantHistory = sessionId.startsWith(QStringLiteral("v_")) && modeType == 1;
    if (modeType == 1 && !isVoiceAssistantHistory) {
        loadTaskSessionHistory(sessionId);
        return;
    }
    if (isVoiceAssistantHistory) {
        // Legacy v_* 旧语音会话：无 Gateway 任务会话，仅展示历史。
        m_chatType = NORMAL;
        QStringList voiceHistory = m_database->selectBySessionID(sessionId);
        voiceHistory = dedupeConsecutiveVoiceErrorMessages(voiceHistory);
        Q_EMIT loadConversation(normalizeVoiceAssistantHistoryMessages(voiceHistory));
        Q_EMIT sessionReplyStatusChanged(sessionId, false, false);
        Q_EMIT modifyDialogueListFlag("");
        return;
    }

    //KyInfo() << "Q_EMIT loadHistoryBySessionId sessionId=" << sessionId;
    QStringList list = filterVoiceMessagesFromTextHistory(m_database->selectBySessionID(sessionId));
    if(m_chat->getSession() != sessionId.toInt()){
        //onPauseRead();//切换对话时，若对话不是当前对话停止朗读//TODO：需要基于readercontrol实现对应逻辑

    }
    m_chat->setSession(sessionId.toInt());
    Q_EMIT changeSessionId(m_chat->getSession());
    Q_EMIT loadConversation(list);
    Q_EMIT sessionReplyStatusChanged(sessionId, false, false);
    Q_EMIT modifyDialogueListFlag("");//恢复删除后列表的删除选中状态

    // 从数据库读取文件引用信息
    QString documentJson = DocumentReferenceDatabase::getInstance()->selectDocumentReferenceBySessionID(sessionId);

    // 如果找到文件引用信息，发送信号到前端
    if (!documentJson.isEmpty()) {
        Q_EMIT restoreDocumentReference(documentJson);
    }

    qDebug() << "loadHistoryBySessionId   initWithHistory ";
    m_chat->initWithHistory(buildChatHistoryFromConversationList(list));

    //切换上下文时，重新设置模型
    m_chat->setNLPModel(ConfigManager::getInstance()->getConfigValue("cur-nlp-model").toString());

}

void CMsgPane::loadTaskSessionHistory(const QString& sessionId)
{
    const QString sessionUuidInDb = m_database->selectSessionUuidBySessionID(sessionId);
    bool isVoiceHistory = isVoiceAssistantSessionUuid(sessionUuidInDb);
    
    // 如果 sessionUuidInDb 不是 "voice-assistant"，检查数据中是否包含 speechFlag 字段
    // 以判断是否是使用 kylin-bot session uuid 存储的语音历史
    if (!isVoiceHistory && !sessionUuidInDb.isEmpty()) {
        QStringList checkList = m_database->selectBySessionID(sessionId);
        for (const QString &item : checkList) {
            QJsonParseError jsonErr;
            const QJsonDocument jsonDoc = QJsonDocument::fromJson(item.toUtf8(), &jsonErr);
            if (jsonErr.error == QJsonParseError::NoError && jsonDoc.isObject()
                    && isVoiceAssistantStoredMessage(jsonDoc.object())) {
                isVoiceHistory = true;
                break;
            }
        }
    }

    QStringList list;
    if (isVoiceHistory) {
        list = dedupeConsecutiveVoiceErrorMessages(m_database->selectBySessionID(sessionId));
        list = normalizeVoiceAssistantHistoryMessages(list);
    } else {
        list = filterVoiceMessagesFromTextHistory(m_database->selectBySessionID(sessionId));
    }

    const QString uuid = taskSessionUuidForLoadedHistory(sessionId, sessionUuidInDb);

    int historyId = sessionId.toInt();

    if (m_taskSessionManager) {
        QString activeUuid = m_taskSessionManager->getActiveSessionUuid();
        if (!activeUuid.isEmpty() && activeUuid != uuid) {
            TaskSession* activeSession = m_taskSessionManager->getSession(activeUuid);
            if (activeSession && activeSession->isReplying) {
                m_taskSessionManager->deactivateSession();
            } else {
                m_backendRecvMessage.remove(activeUuid);
                m_taskSessionManager->closeSession(activeUuid);
            }
        }
    }

    TaskSession* session = m_taskSessionManager->getSession(uuid);

    if (!session) {
        m_taskSessionManager->createSession(uuid, historyId);
        session = m_taskSessionManager->getSession(uuid);
    } else {
        m_taskSessionManager->activateSession(uuid);
    }

    const bool isReplying = session && session->isReplying;
    Q_EMIT updateTaskSessionIsReplying(isReplying);

    new_reply = true;
    recv_reasoning_message = "";
    m_chatType = TASK_MODE;

    m_chat->setSession(historyId);
    Q_EMIT changeSessionId(historyId);
    Q_EMIT changeTaskSessionId(m_taskSessionManager->getActiveSessionUuid());

    Q_EMIT loadConversation(normalizeTaskSessionHistoryMessages(list));
    Q_EMIT sessionReplyStatusChanged(sessionId, isReplying, false);

    if (m_backendRecvMessage.contains(uuid) && !m_backendRecvMessage[uuid].isEmpty()) {
        QString header = m_backendRecvMessageHeader.value(uuid, "");
        pushMsg("addRecvMsg", header, m_backendRecvMessage[uuid]);
        if (m_backendTaskToolCalls.contains(uuid)) {
            for (const QJsonValue &toolFrame : m_backendTaskToolCalls[uuid]) {
                if (toolFrame.isObject()) {
                    Q_EMIT updateTaskToolCalls(QString::fromUtf8(QJsonDocument(toolFrame.toObject()).toJson(QJsonDocument::Compact)));
                }
            }
        }
        recv_reasoning_message = m_backendRecvReasoningMsg.value(uuid, "");
        if (!recv_reasoning_message.isEmpty()) {
            pushMsg("updateReasoningMsg", "", recv_reasoning_message);
        }
        new_reply = false;
    } else if (session && session->isReplying) {
        // Session is still replying but only tool calls cached (no text yet).
        // Create an empty Bot placeholder so the first real-time updateMsg
        // does not overwrite the User message (loaded from DB with isFirst=true).
        QJsonObject header;
        header["modelMsg"] = tr("Content generated by AI");
        pushMsg("addRecvMsg", QJsonDocument(header).toJson(QJsonDocument::Compact), tr("Answering"));
        if (m_backendTaskToolCalls.contains(uuid)) {
            for (const QJsonValue &toolFrame : m_backendTaskToolCalls[uuid]) {
                if (toolFrame.isObject()) {
                    Q_EMIT updateTaskToolCalls(QString::fromUtf8(QJsonDocument(toolFrame.toObject()).toJson(QJsonDocument::Compact)));
                }
            }
        }
        recv_reasoning_message = m_backendRecvReasoningMsg.value(uuid, "");
        if (!recv_reasoning_message.isEmpty()) {
            pushMsg("updateReasoningMsg", "", recv_reasoning_message);
        }
        new_reply = false;
    }

    // 加载对话历史上下文（语音通话历史恢复时，通过 history 注入模型上下文）
    m_chat->initWithHistory(buildChatHistoryFromConversationList(list));
}

QString CMsgPane::extractDocumentReferenceFromHistory(const QString &sessionId)
{
    // 从数据库读取文件引用信息（已迁移到 DocumentReferenceDatabase 类）
    return DocumentReferenceDatabase::getInstance()->selectDocumentReferenceBySessionID(sessionId);
}

QString CMsgPane::getConfigValue(const QString &key)
{
    return ConfigManager::getInstance()->getConfigValue(key).toString();
}

void CMsgPane::updateConfig(const QString &key, const QVariant &value)
{
    ConfigManager::getInstance()->updateConfig(key, value);
}

void CMsgPane::syncGatewayPairing()
{
    QString token = ConfigManager::getInstance()->getConfigValue("gateway-token").toString().trimmed();
    if (!token.isEmpty()) {
        return;
    }

    UnixHttpResponse r1 = requestGatewaySocketFirst(
        QStringLiteral("POST"), QStringLiteral("/admin/paircode/new"));
    if (r1.statusCode == 0) {
        KyInfo() << "syncGatewayPairing: pair code request failed:" << r1.errorString;
        return;
    }

    if (r1.statusCode != 200 && r1.statusCode != 400) {
        KyInfo() << "syncGatewayPairing: pair code unexpected status" << r1.statusCode
                 << "body:" << QString::fromUtf8(r1.body).left(500);
        return;
    }

    QJsonDocument doc1 = QJsonDocument::fromJson(r1.body);
    if (!doc1.isObject()) {
        KyInfo() << "syncGatewayPairing: invalid paircode response, body:"
                 << QString::fromUtf8(r1.body).left(200);
        return;
    }

    QJsonObject obj1 = doc1.object();
    const QJsonValue pairingRequiredValue = obj1.value(QStringLiteral("pairing_required"));
    if (pairingRequiredValue.isBool() && !pairingRequiredValue.toBool()) {
        KyInfo() << "syncGatewayPairing: pairing not required";
        return;
    }

    QString code = obj1.value("pairing_code").toString();
    if (code.isEmpty()) {
        KyInfo() << "syncGatewayPairing: no pairing code available";
        return;
    }

    KyInfo() << "syncGatewayPairing: pairing code obtained:" << code;

    QMap<QString, QString> headers;
    headers[QStringLiteral("X-Pairing-Code")] = code;
    UnixHttpResponse r2 = requestGatewaySocketFirst(
        QStringLiteral("POST"), QStringLiteral("/pair"), headers);
    if (r2.statusCode == 0) {
        KyInfo() << "syncGatewayPairing: pairing failed:" << r2.errorString;
        return;
    }

    if (r2.statusCode != 200) {
        KyInfo() << "syncGatewayPairing: pairing unexpected status" << r2.statusCode
                 << "body:" << QString::fromUtf8(r2.body).left(500);
        return;
    }

    QJsonDocument doc2 = QJsonDocument::fromJson(r2.body);
    if (!doc2.isObject()) {
        KyInfo() << "syncGatewayPairing: invalid pairing response, body:"
                 << QString::fromUtf8(r2.body).left(200);
        return;
    }

    QJsonObject obj2 = doc2.object();
    if (obj2.contains("token")) {
        QString newToken = obj2["token"].toString();
        ConfigManager::getInstance()->updateConfig("gateway-token", newToken);
        KyInfo() << "syncGatewayPairing: succeeded, token saved";
        return;
    }

    KyInfo() << "syncGatewayPairing: pairing response missing token";
}

bool CMsgPane::validateAndRefreshGatewayToken()
{
    QString token = ConfigManager::getInstance()->getConfigValue("gateway-token").toString().trimmed();
    if (token.isEmpty()) {
        syncGatewayPairing();
        return true;
    }

    QMap<QString, QString> headers;
    headers[QStringLiteral("Authorization")] = QStringLiteral("Bearer ") + token;
    UnixHttpResponse resp = requestGatewaySocketFirst(
        QStringLiteral("GET"), QStringLiteral("/api/health"), headers);

    if (resp.statusCode == 200) {
        KyInfo() << "validateAndRefreshGatewayToken: token is valid";
        return true;
    }

    if (resp.statusCode == 401) {
        KyInfo() << "validateAndRefreshGatewayToken: token invalidated (401), re-pairing";
        ConfigManager::getInstance()->updateConfig("gateway-token", "");
        syncGatewayPairing();
        return true;
    }

    if (resp.statusCode == 0) {
        KyInfo() << "validateAndRefreshGatewayToken: gateway unreachable, assuming token still valid:"
                 << resp.errorString;
        return true;
    }

    KyInfo() << "validateAndRefreshGatewayToken: unexpected status" << resp.statusCode
             << "body:" << QString::fromUtf8(resp.body).left(200)
             << "- assuming token still valid";
    return true;
}





void CMsgPane::getPluginList()
{
    Q_EMIT loadPluginList(m_pluginList);
}

void CMsgPane::getDialogueList()
{
    QStringList list = m_database->selectAllDialogue();

    QString activeNormalSessionId;
    bool nonTaskChatReplying = false;
    if (m_isReplying && m_chatType != TASK_MODE && m_chat) {
        int sid = m_chat->getSession();
        if (sid > 0) {
            activeNormalSessionId = QString::number(sid);
            nonTaskChatReplying = true;
        }
    }

    for (int i = 0; i < list.size(); ++i) {
        QJsonDocument doc = QJsonDocument::fromJson(list[i].toUtf8());
        if (!doc.isObject()) continue;
        QJsonObject obj = doc.object();
        int modeType = obj.value("modeType").toInt(0);

        if (modeType == 1) {
            if (m_taskSessionManager) {
                QString sessionID = obj.value("sessionID").toString();
                TaskSession* session = m_taskSessionManager->getSessionByHistoryId(sessionID.toInt());
                if (session && session->isReplying) {
                    obj["isReplying"] = true;
                    list[i] = QJsonDocument(obj).toJson(QJsonDocument::Compact);
                }
            }
        } else {
            if (nonTaskChatReplying) {
                QString sessionID = obj.value("sessionID").toString();
                if (sessionID == activeNormalSessionId) {
                    obj["isReplying"] = true;
                    list[i] = QJsonDocument(obj).toJson(QJsonDocument::Compact);
                }
            }
        }
    }

    Q_EMIT loadDialogueList(list);
}

void CMsgPane::modifyDialogueList(const QVariantList &strings)
{
    for (const QVariant &sessionIDs : strings) {
        QString sessionID = sessionIDs.toString();
        Q_EMIT modifyDialogueListFlag(sessionID);
    }
}

void CMsgPane::deleteSessionList(const QVariantList &strings)
{
    // 删除选中历史记录
    // 用户点击了取消按钮或关闭了消息框，执行取消操作
    //KyInfo() << "deleteSessionList";
    QStringList deleteList;
    for (const QVariant &sessionIDs : strings) {
        QString sessionID = sessionIDs.toString();
        KyInfo() << "删除的sessionID为:" << sessionID;
        if(sessionID == QString::number(m_chat->getSession())){
            // stopreply之后不会触发自动朗读
            m_chat->soundFlag = false;
            onStopReply();
            Q_EMIT VoiceUsingManager::getInstance()->sigInterruptUsing(false);

            Q_EMIT clearListModel();

            QTimer *tempTimer = new QTimer();
            connect(tempTimer,&QTimer::timeout,[&]{
                m_chat->clearContext();
                m_chat->setSession(getCurrentMaxHistoryId());
                Q_EMIT changeSessionId(m_chat->getSession());
            });
            tempTimer->setSingleShot(true);
            tempTimer->start(1000);
        }
        // 任务会话清理：通过数据库modeType判断是否为任务模式会话，
        // 先关闭本地TaskSession和清理缓存，再通知后端删除会话数据
        if (m_taskSessionManager) {
            int modeType = m_database->getSessionModeType(sessionID);
            if (modeType == 1) {
                QString sessionUuid = m_database->selectSessionUuidBySessionID(sessionID);

                bool isCurrentlyActive = (sessionID == QString::number(m_chat->getSession()));
                if (isCurrentlyActive) {
                    QString activeUuid = m_taskSessionManager->getActiveSessionUuid();
                    if (!activeUuid.isEmpty()) {
                        m_taskSessionManager->cancelResponse(activeUuid);
                        clearBackendRecvCache(activeUuid);
                        m_taskSessionManager->closeSession(activeUuid);
                    }
                    m_chatType = NORMAL;
                } else {
                    TaskSession* taskSession = m_taskSessionManager->getSessionByHistoryId(sessionID.toInt());
                    if (taskSession) {
                        m_taskSessionManager->cancelResponse(taskSession->sessionUuid);
                        clearBackendRecvCache(taskSession->sessionUuid);
                        m_taskSessionManager->closeSession(taskSession->sessionUuid);
                        Q_EMIT sessionReplyStatusChanged(sessionID, false, false);
                    }
                }

                m_taskSessionManager->deleteBackendSession(sessionUuid);
            }
        }

        m_database->deleteBySessionID(sessionID);
        // 同时删除对应的文件引用记录
        DocumentReferenceDatabase::getInstance()->deleteDocumentReferenceBySessionID(sessionID);
        deleteList.append(sessionID);
    }
    //调用前端页面删除
    Q_EMIT deleteDialogueList(deleteList);
}

QString CMsgPane::convertMarkDownToHtml(const QString &markDown)
{
    // 将markDown格式转换为html格式
    if(markDown.length() < 1){
        return "";
    }
    QString html = markdownToHtml(markDown.toStdString());
    return ensureCodeBlockPreTagsWrap(html);
}

int CMsgPane::getNextHistoryId()
{
    int history_id_database = m_database->selectMaxHistoryId();
    int history_id_settings = ConfigManager::getInstance()->getConfigValue("history-id").toInt();
    int historyId = (history_id_database >= history_id_settings ? history_id_database : history_id_settings) + 1;
    m_database->updateMaxHistoryId(historyId);
    return historyId;
}

int CMsgPane::getCurrentMaxHistoryId()
{
    int history_id_database = m_database->selectMaxHistoryId();
    int history_id_settings = ConfigManager::getInstance()->getConfigValue("history-id").toInt();
    return (history_id_database >= history_id_settings ? history_id_database : history_id_settings);
}

void CMsgPane::deactivateTaskSession()
{
    if (m_taskSessionManager) {
        m_taskSessionManager->deactivateSession();
    }
    recv_message = "";
    recv_reasoning_message = "";
    new_reply = true;
    m_chatType = NORMAL;
    m_chat->setSession(getCurrentMaxHistoryId());
    Q_EMIT changeSessionId(m_chat->getSession());
}

int CMsgPane::getSessionModeType(const QString& sessionId)
{
    return m_database->getSessionModeType(sessionId);
}

void CMsgPane::closeSession()
{
    //onPauseRead();//TODO:需要基于readercontrol实现朗读停止效果
    if (m_chatType == TASK_MODE) {
        if (m_taskSessionManager) {
            QString activeUuid = m_taskSessionManager->getActiveSessionUuid();
            if (!activeUuid.isEmpty()) {
                m_backendRecvMessage.remove(activeUuid);
                m_taskSessionManager->closeSession(activeUuid);
            }
        }
        recv_message = "";
        recv_reasoning_message = "";
        new_reply = true;
        m_chatType = NORMAL;
        m_chat->setSession(getCurrentMaxHistoryId());
        Q_EMIT changeSessionId(m_chat->getSession());
    } else {
        m_chat->setSession(getCurrentMaxHistoryId());
        onClearContext();
        Q_EMIT changeSessionId(m_chat->getSession());
    }
}


bool CMsgPane::getTaskSessionIsReplying(const QString& sessionUuid)
{
    if (m_taskSessionManager) {
        TaskSession* session = m_taskSessionManager->getSession(sessionUuid);
        if (session) {
            return session->isReplying;
        }
    }
    return false;
}

int CMsgPane::getTaskSessionCount() const
{
    if (m_taskSessionManager) {
        return m_taskSessionManager->sessionCount();
    }
    return 0;
}


// 超时处理已移至前端 QML 端

// ============================================================================
// kybot cron 历史同步：把 KylinBot 的 cron 运行结果导入为本地 task-mode 历史
// 详见 scheduled/kybotcronsyncmanager.* 与计划 kybot-cron-history-sync
// ============================================================================

struct ParsedKybotCronRunOutput {
    QString message;
    QString thinking;
    QJsonArray toolCalls;
};

static bool parseLooseJsonStringAt(const QString& text, int quotePos, QString& out)
{
    if (quotePos < 0 || quotePos >= text.size() || text.at(quotePos) != QLatin1Char('"')) {
        return false;
    }

    QString decoded;
    decoded.reserve(text.size() - quotePos);

    for (int i = quotePos + 1; i < text.size(); ++i) {
        const QChar ch = text.at(i);
        if (ch == QLatin1Char('"')) {
            out = decoded;
            return true;
        }
        if (ch != QLatin1Char('\\')) {
            decoded.append(ch);
            continue;
        }

        ++i;
        if (i >= text.size()) {
            return false;
        }

        const QChar esc = text.at(i);
        switch (esc.unicode()) {
        case '"':
            decoded.append(QLatin1Char('"'));
            break;
        case '\\':
            decoded.append(QLatin1Char('\\'));
            break;
        case '/':
            decoded.append(QLatin1Char('/'));
            break;
        case 'b':
            decoded.append(QLatin1Char('\b'));
            break;
        case 'f':
            decoded.append(QLatin1Char('\f'));
            break;
        case 'n':
            decoded.append(QLatin1Char('\n'));
            break;
        case 'r':
            decoded.append(QLatin1Char('\r'));
            break;
        case 't':
            decoded.append(QLatin1Char('\t'));
            break;
        case 'u': {
            if (i + 4 >= text.size()) {
                return false;
            }
            bool ok = false;
            const ushort code = text.mid(i + 1, 4).toUShort(&ok, 16);
            if (!ok) {
                return false;
            }
            decoded.append(QChar(code));
            i += 4;
            break;
        }
        default:
            decoded.append(esc);
            break;
        }
    }

    return false;
}

static QString extractLooseJsonStringField(const QString& text,
                                           const QString& fieldName,
                                           int searchStart = 0)
{
    const QString marker = QStringLiteral("\"%1\"").arg(fieldName);
    int fieldPos = text.indexOf(marker, searchStart);
    while (fieldPos >= 0) {
        const int colonPos = text.indexOf(QLatin1Char(':'), fieldPos + marker.size());
        if (colonPos < 0) {
            return QString();
        }

        int valuePos = colonPos + 1;
        while (valuePos < text.size() && text.at(valuePos).isSpace()) {
            ++valuePos;
        }

        if (valuePos < text.size() && text.at(valuePos) == QLatin1Char('"')) {
            QString value;
            if (parseLooseJsonStringAt(text, valuePos, value)) {
                return value;
            }
            return QString();
        }

        fieldPos = text.indexOf(marker, fieldPos + marker.size());
    }

    return QString();
}

QString CMsgPane::buildKybotCronUserMessageJson(const QString& jobId,
                                                const QString& jobName,
                                                const QString& jobPrompt,
                                                const QString& runId) const
{
    QJsonObject json;
    json["author"] = "User";
    json["message"] = jobPrompt;
    json["msgHead"] = jobName;
    json["isEnd"] = true;
    json["isScheduled"] = true;
    json["taskId"] = jobId;
    json["runId"] = runId;
    fillKybotChatDefaults(json);
    return QString::fromUtf8(QJsonDocument(json).toJson(QJsonDocument::Compact));
}

static ParsedKybotCronRunOutput parseKybotCronRunOutput(const QString& rawOutput)
{
    ParsedKybotCronRunOutput parsed;
    parsed.message = rawOutput;
    const int wrapperPos = rawOutput.indexOf(QStringLiteral("\"kylin_cron_v1\""));

    QJsonParseError jsonErr;
    const QJsonDocument doc = QJsonDocument::fromJson(rawOutput.toUtf8(), &jsonErr);
    if (jsonErr.error != QJsonParseError::NoError || !doc.isObject()) {
        if (wrapperPos >= 0) {
            const QString looseMessage =
                extractLooseJsonStringField(rawOutput, QStringLiteral("message"), wrapperPos);
            if (!looseMessage.isEmpty()) {
                parsed.message = looseMessage;
            }

            parsed.thinking =
                extractLooseJsonStringField(rawOutput, QStringLiteral("thinking"), wrapperPos);
            parsed.thinking.replace(QStringLiteral("<"), QStringLiteral("&lt;"));
            parsed.thinking.replace(QStringLiteral(">"), QStringLiteral("&gt;"));
        }
        return parsed;
    }

    const QJsonObject root = doc.object();
    const QJsonObject payload = root.value(QStringLiteral("kylin_cron_v1")).toObject();
    if (payload.isEmpty()) {
        if (wrapperPos < 0) {
            return parsed;
        }

        const QString looseMessage =
            extractLooseJsonStringField(rawOutput, QStringLiteral("message"), wrapperPos);
        if (!looseMessage.isEmpty()) {
            parsed.message = looseMessage;
        }

        parsed.thinking =
            extractLooseJsonStringField(rawOutput, QStringLiteral("thinking"), wrapperPos);
        parsed.thinking.replace(QStringLiteral("<"), QStringLiteral("&lt;"));
        parsed.thinking.replace(QStringLiteral(">"), QStringLiteral("&gt;"));
        return parsed;
    }

    parsed.message = payload.value(QStringLiteral("message")).toString(rawOutput);
    parsed.thinking = payload.value(QStringLiteral("thinking")).toString();
    parsed.toolCalls = payload.value(QStringLiteral("tool_calls")).toArray();
    parsed.thinking.replace(QStringLiteral("<"), QStringLiteral("&lt;"));
    parsed.thinking.replace(QStringLiteral(">"), QStringLiteral("&gt;"));
    return parsed;
}

QString CMsgPane::buildKybotCronBotMessageJson(const QString& runOutput,
                                               const QString& runStatus,
                                               const QString& reasonMessage,
                                               const QString& taskToolCalls) const
{
    QJsonObject json;
    json["author"] = "Bot";
    json["isEnd"] = true;
    json["message"] = runOutput;
    json["modelMsg"] = modelMsgForTaskMode();
    json["isScheduled"] = true;
    json["status"] = runStatus;
    if (!reasonMessage.isEmpty()) {
        json["reasonMessage"] = reasonMessage;
    }
    if (!taskToolCalls.isEmpty()) {
        json["taskToolCalls"] = taskToolCalls;
    }
    fillKybotChatDefaults(json);
    return QString::fromUtf8(QJsonDocument(json).toJson(QJsonDocument::Compact));
}

QString CMsgPane::buildKybotCronBotMessageJson(const QString& runOutput,
                                               const QString& runStatus) const
{
    return buildKybotCronBotMessageJson(runOutput, runStatus, QString(), QString());
}

bool CMsgPane::importKybotCronRun(const QString& jobId,
                                  const QString& jobName,
                                  const QString& jobPrompt,
                                  const QString& runId,
                                  const QString& runOutput,
                                  const QString& runStatus,
                                  qint64 startedAtSecs)
{
    if (!m_database) {
        return false;
    }
    if (runId.isEmpty()) {
        return false;
    }

    const QString sessionID = QStringLiteral("kybot-cron:%1").arg(runId);
    if (m_database->sessionExists(sessionID)) {
        return false;
    }

    const QString userJson = buildKybotCronUserMessageJson(jobId, jobName, jobPrompt, runId);

    const ParsedKybotCronRunOutput parsedOutput = parseKybotCronRunOutput(runOutput);
    QString taskToolCallsJson;
    if (!parsedOutput.toolCalls.isEmpty()) {
        QJsonObject toolCallsObj;
        toolCallsObj["toolCalls"] = parsedOutput.toolCalls;
        taskToolCallsJson = QString::fromUtf8(
            QJsonDocument(toolCallsObj).toJson(QJsonDocument::Compact));
    }
    const QString botJson = buildKybotCronBotMessageJson(
        parsedOutput.message, runStatus, parsedOutput.thinking, taskToolCallsJson);

    // 两条 RECORD 行：msgIndex=0 为 user prompt，msgIndex=1 为 bot output
    // mode_type=1 复用 task mode 展示，chat_type=0
    if (!m_database->insertOneWithModeType(sessionID, 0, userJson, 0, 1)) {
        KyInfo() << "importKybotCronRun: failed to insert user row, sessionID=" << sessionID;
        return false;
    }
    if (!m_database->insertOneWithModeType(sessionID, 1, botJson, 0, 1)) {
        KyInfo() << "importKybotCronRun: failed to insert bot row, sessionID=" << sessionID;
        // 回滚 user 行避免半截会话
        m_database->deleteBySessionID(sessionID);
        return false;
    }

    if (startedAtSecs > 0) {
        m_database->setOperateTime(sessionID, 0, startedAtSecs);
        m_database->setOperateTime(sessionID, 1, startedAtSecs);
    }

    m_database->setUnreadFlag(sessionID, 1);
    m_database->markSessionSource(sessionID,
        QStringLiteral("scheduler:kybot:%1:%2").arg(jobId, runId));

    KyInfo() << "importKybotCronRun: inserted" << sessionID
             << "job=" << jobName << "status=" << runStatus;
    return true;
}

void CMsgPane::onKybotCronRunImported(const QString& sessionUuid)
{
    // 通知 QML 刷新历史列表（HistoryShowPage.qml 中已挂 onScheduledSessionCreated）
    Q_EMIT scheduledSessionCreated(sessionUuid);
}
