Qt实现CS的自动化构建流程

一、简介

基于Qt框架实现了一套客户端、服务器架构的自动化构建与文件传输系统,核心功能是服务端触发构建任务、客户端执行构建并上传产物,同时支持产物下载、状态监控等能力。

测试服务器的基本指令

1、测试健康检查

curl.exe "http://127.0.0.1:8080/health?token=build_secret_token_2026"

2、查询状态

curl.exe "http://127.0.0.1:8080/stats?token=build_secret_token_2026"

3、测试轮询

curl http://127.0.0.1:8080/poll?agent=win_tools\&token=build_secret_token_2026

4、触发构建

curl.exe -X POST http://127.0.0.1:8080/trigger -H "Content-Type: application/json" -d "{\"type\":\"win_tools\",\"branch\":\"main\",\"token\":\"build_secret_token_2026\"}"

curl.exe -X POST http://127.0.0.1:8080/trigger -H "Content-Type: application/json" -d "{\"type\":\"win_services\",\"branch\":\"main\",\"token\":\"build_secret_token_2026\"}"

curl.exe -X POST http://127.0.0.1:8080/trigger -H "Content-Type: application/json" -d "{\"type\":\"arm_sim\",\"branch\":\"main\",\"token\":\"build_secret_token_2026\"}"

curl.exe -X POST http://127.0.0.1:8080/trigger -H "Content-Type: application/json" -d "{\"type\":\"linux_x86\",\"branch\":\"main\",\"token\":\"build_secret_token_2026\"}"

5、发布状态

curl.exe "http://127.0.0.1:8080/download?agent=win_tools\&token=build_secret_token_2026" --output win_tool

6、下载

curl.exe "http://127.0.0.1:8080/download?agent=win_tools\&token=build_secret_token_2026" --output win_tool.zip

二、详解
1、服务端

(1)build_server.pro

复制代码
# build_server.pro

QT       += core network
QT       -= gui

TARGET = BuildServer
TEMPLATE = app

CONFIG += c++11 console automoc
CONFIG -= qt_quickcompiler

VERSION = 1.0.0
DEFINES += VERSION=\"\\\"$${VERSION}\\\"\"

unix {
    DEFINES += Q_OS_LINUX
    QMAKE_CXXFLAGS += -Wall -Wextra
    LIBS += -lpthread
} else:win32 {
    DEFINES += Q_OS_WIN
    LIBS += -lws2_32
}

INCLUDEPATH += $$PWD/include

SOURCES += \
    src/main.cpp \
    src/http_server.cpp

HEADERS += \
    include/http_server.h

DESTDIR = $$PWD/bin
QMAKE_CLEAN += bin/* obj/*

(2)http_server.h

复制代码
#ifndef HTTP_SERVER_H
#define HTTP_SERVER_H

#include <QObject>
#include <QTcpServer>
#include <QTcpSocket>
#include <QMap>
#include <QMutex>
#include <QTimer>
#include <QFile>
#include <QDir>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QDateTime>
#include <QStandardPaths>
#include <QByteArray>
#include <QString>
#include <QDebug>
#include <QUrlQuery>
#include <QCryptographicHash>

// 上传上下文结构
struct UploadContext {
    QFile *file;
    QString type;
    QString filename;
    qint64 size;
    qint64 received;
    qint64 sinceLastFlush;   // 距上次 flush 已累积字节数
    UploadContext() : file(nullptr), size(0), received(0), sinceLastFlush(0) {}

    ~UploadContext() {
        if (file) {
            if (file->isOpen()) file->close();
            delete file;
        }
    }
};

struct BuildTask {
    QString type;
    QString branch;
    bool requested = false;
    bool in_progress = false;
    bool completed = false;
    QString status = "idle";
    QString lastBuildTime;
    int requestCount = 0;

    BuildTask() : type(""), branch("main"), requested(false),
                  in_progress(false), completed(false), status("idle"),
                  lastBuildTime(""), requestCount(0) {}

    BuildTask(const QString &t, const QString &b, bool req, bool prog,
              bool comp, const QString &st, const QString &time, int cnt)
        : type(t), branch(b), requested(req), in_progress(prog),
          completed(comp), status(st), lastBuildTime(time), requestCount(cnt) {}
};

struct ReleaseFile {
    QString type;
    QString filename;
    QString filepath;
    QString md5;
    qint64 size;
    QString version;
    QString updateTime;

    ReleaseFile() : size(0), version("1.0.0"), updateTime("") {}

    ReleaseFile(const QString &t, const QString &fn, const QString &fp,
                const QString &m, qint64 s, const QString &v, const QString &ut)
        : type(t), filename(fn), filepath(fp), md5(m), size(s), version(v), updateTime(ut) {}
};

class HttpServer : public QObject {
    Q_OBJECT

public:
    explicit HttpServer(quint16 port = 8080, QObject *parent = nullptr);
    ~HttpServer();

    void start();
    void stop();
    void triggerBuild(const QString &type, const QString &branch = "main");
    void triggerDownload(const QString &type, const QString &version);
    QString getStatus(const QString &type);
    QByteArray getAllStatus();
    QByteArray getReleaseInfo(const QString &type);

private slots:
    void handleNewConnection();
    void readClient();
    void discardClient();
    void handleSocketError(QAbstractSocket::SocketError socketError);

private:
    void finishUpload(QTcpSocket *socket, UploadContext *context);
    void cleanupUploadContext(QTcpSocket *socket);

    void initializeTasks();
    void initializeReleases();
    QString getReleaseDir(const QString &type);
    void saveBuildLog(const QString &type, const QString &message);
    bool authenticate(const QString &token);
    QByteArray parseHttpRequest(const QByteArray &data);
    QByteArray handlePoll(const QUrlQuery &query);
    QByteArray handleUpload(const QUrlQuery &query, const QByteArray &data, QTcpSocket *socket);
    void handleTrigger(const QByteArray &body);
    QByteArray handleHealth();
    QByteArray handleStats();
    QByteArray handleDownload(const QUrlQuery &query);
    QByteArray handleReleaseInfo(const QUrlQuery &query);
    QString calculateMD5(const QString &filePath);
    void saveReleaseInfo(const QString &type);
    QByteArray getReleaseJson();

    quint16 port_;
    QTcpServer tcpServer_;
    QMap<QString, BuildTask> tasks_;
    QMap<QString, ReleaseFile> releases_;
    QMutex mutex_;
    QMutex releaseMutex_;
    QString releaseDir_;
    QString apiToken_;
    bool running_ = false;

    QMap<QTcpSocket*, UploadContext*> uploadFiles_;
    QMap<QTcpSocket*, QString> uploadTypes_;
    QMap<QTcpSocket*, QString> uploadFilenames_;
    QMap<QTcpSocket*, QByteArray> uploadMd5_;
    QMap<QTcpSocket*, qint64> uploadSize_;
    QMap<QTcpSocket*, qint64> uploadReceived_;

    static constexpr const char *DEFAULT_TOKEN = "build_secret_token_2026";
};

#endif // HTTP_SERVER_H

(3)http_server.cpp

复制代码
#include "http_server.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QFile>
#include <QDir>
#include <QDateTime>
#include <QUrlQuery>
#include <QHostAddress>
#include <QDebug>
#include <QCryptographicHash>
#include <QCoreApplication>

#ifdef Q_OS_LINUX
#include <sys/socket.h>
#include <netinet/tcp.h>
#endif

#ifdef Q_OS_WIN
#include <winsock2.h>
#include <windows.h>
#endif

// 上传进度刷新间隔(每接收 1MB flush 一次)
static const qint64 FLUSH_INTERVAL_BYTES = 1024 * 1024;
// 下载块大小(256KB)
static const qint64 DOWNLOAD_CHUNK_SIZE = 256 * 1024;

HttpServer::HttpServer(quint16 port, QObject *parent)
    : QObject(parent)
    , port_(port)
    , releaseDir_(QCoreApplication::applicationDirPath() + "/build_release")
    , apiToken_(DEFAULT_TOKEN)
    , running_(false)
{
    initializeTasks();
    initializeReleases();
    QDir dir(releaseDir_);
    if (!dir.exists()) dir.mkpath(".");
    connect(&tcpServer_, &QTcpServer::newConnection, this, &HttpServer::handleNewConnection);
}

HttpServer::~HttpServer() {
    stop();
    for (auto *ctx : uploadFiles_) {
        if (ctx) {
            if (ctx->file) {
                ctx->file->close();
                delete ctx->file;
            }
            delete ctx;
        }
    }
    uploadFiles_.clear();
}

void HttpServer::initializeTasks() {
    tasks_["win_tools"] = BuildTask("win_tools", "main", false, false, false, "idle", "", 0);
    tasks_["win_services"] = BuildTask("win_services", "main", false, false, false, "idle", "", 0);
    tasks_["arm_sim"] = BuildTask("arm_sim", "main", false, false, false, "idle", "", 0);
    tasks_["linux_x86"] = BuildTask("linux_x86", "main", false, false, false, "idle", "", 0);
}

void HttpServer::initializeReleases() {
    releases_["win_tools"] = ReleaseFile("win_tools", "", "", "", 0, "", "");
    releases_["win_services"] = ReleaseFile("win_services", "", "", "", 0, "", "");
    releases_["arm_sim"] = ReleaseFile("arm_sim", "", "", "", 0, "", "");
    releases_["linux_x86"] = ReleaseFile("linux_x86", "", "", "", 0, "", "");
}

void HttpServer::start() {
    if (running_) return;
    if (tcpServer_.listen(QHostAddress::Any, port_)) {
        running_ = true;
        qDebug() << "[Server] Started on port" << port_;
    } else {
        qCritical() << "[Server] Failed to start:" << tcpServer_.errorString();
    }
}

void HttpServer::stop() {
    if (!running_) return;
    tcpServer_.close();
    running_ = false;
    qDebug() << "[Server] Stopped";
}

void HttpServer::triggerBuild(const QString &type, const QString &branch) {
    QMutexLocker locker(&mutex_);
    if (!tasks_.contains(type)) return;
    BuildTask &task = tasks_[type];
    task.requested = true;
    task.branch = branch;
    task.status = "pending";
    task.requestCount++;
    task.completed = false;
    task.in_progress = false;
    qDebug() << "[Server] Build triggered:" << type << branch;
    saveBuildLog(type, "Build triggered for branch: " + branch);
}

void HttpServer::triggerDownload(const QString &type, const QString &version) {
    QMutexLocker locker(&releaseMutex_);
    if (!releases_.contains(type)) return;
    ReleaseFile &release = releases_[type];
    release.version = version;
    release.updateTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
}

QString HttpServer::getStatus(const QString &type) {
    QMutexLocker locker(&mutex_);
    if (!tasks_.contains(type)) return QJsonDocument(QJsonObject{{"status","unknown"}}).toJson();
    const BuildTask &t = tasks_[type];
    QJsonObject obj;
    obj["type"]=t.type; obj["status"]=t.status; obj["requested"]=t.requested;
    obj["in_progress"]=t.in_progress; obj["completed"]=t.completed;
    obj["branch"]=t.branch; obj["lastBuildTime"]=t.lastBuildTime; obj["requestCount"]=t.requestCount;
    return QJsonDocument(obj).toJson();
}

QByteArray HttpServer::getAllStatus() {
    QMutexLocker locker(&mutex_);
    QJsonObject root;
    for (auto &t : tasks_) {
        QJsonObject obj;
        obj["status"]=t.status; obj["requested"]=t.requested; obj["in_progress"]=t.in_progress;
        obj["completed"]=t.completed; obj["branch"]=t.branch; obj["lastBuildTime"]=t.lastBuildTime;
        obj["requestCount"]=t.requestCount;
        root[t.type]=obj;
    }
    return QJsonDocument(root).toJson();
}

QByteArray HttpServer::getReleaseInfo(const QString &type) {
    QMutexLocker locker(&releaseMutex_);
    if (!releases_.contains(type)) return QJsonDocument(QJsonObject{{"status","unknown"}}).toJson();
    const ReleaseFile &r = releases_[type];
    QJsonObject obj;
    obj["type"]=r.type; obj["filename"]=r.filename; obj["md5"]=r.md5; obj["size"]=r.size;
    obj["version"]=r.version; obj["updateTime"]=r.updateTime;
    return QJsonDocument(obj).toJson();
}

bool HttpServer::authenticate(const QString &token) { return token == apiToken_; }
QString HttpServer::getReleaseDir(const QString &type) { return releaseDir_ + "/" + type; }

void HttpServer::saveBuildLog(const QString &type, const QString &message) {
    QString logPath = getReleaseDir(type) + "/build.log";
    QFile f(logPath);
    if (f.open(QIODevice::Append|QIODevice::Text)) {
        f.write(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss").toUtf8() + " - " + message.toUtf8() + "\n");
        f.close();
    }
}

QString HttpServer::calculateMD5(const QString &filePath) {
    QFile f(filePath);
    if (!f.open(QIODevice::ReadOnly)) return QString();
    QCryptographicHash hash(QCryptographicHash::Md5);
    while (!f.atEnd()) hash.addData(f.read(8192));
    f.close();
    return hash.result().toHex();
}

void HttpServer::saveReleaseInfo(const QString &type) {
    if (!releases_.contains(type)) return;
    ReleaseFile &r = releases_[type];
    QString path = getReleaseDir(type) + "/release_info.json";
    QJsonObject obj;
    obj["type"]=r.type; obj["filename"]=r.filename; obj["md5"]=r.md5; obj["size"]=r.size;
    obj["version"]=r.version; obj["updateTime"]=r.updateTime;
    QFile f(path);
    if (f.open(QIODevice::WriteOnly)) f.write(QJsonDocument(obj).toJson());
}

QByteArray HttpServer::getReleaseJson() {
    QMutexLocker locker(&releaseMutex_);
    QJsonObject root;
    for (auto &r : releases_) {
        QJsonObject obj;
        obj["filename"]=r.filename; obj["md5"]=r.md5; obj["size"]=r.size;
        obj["version"]=r.version; obj["updateTime"]=r.updateTime;
        root[r.type]=obj;
    }
    return QJsonDocument(root).toJson();
}

void HttpServer::handleNewConnection() {
    while (tcpServer_.hasPendingConnections()) {
        QTcpSocket *socket = tcpServer_.nextPendingConnection();
        // 基础跨平台选项
        socket->setSocketOption(QAbstractSocket::ReceiveBufferSizeSocketOption, 10*1024*1024);
        socket->setSocketOption(QAbstractSocket::KeepAliveOption, 1);
        socket->setSocketOption(QAbstractSocket::LowDelayOption, 1); // 禁用 Nagle

#ifdef Q_OS_LINUX
        int fd = socket->socketDescriptor();
        // 更精细的 TCP keep-alive 参数
        int keepIdle = 10, keepIntvl = 5, keepCnt = 3;
        setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &keepIdle, sizeof(keepIdle));
        setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &keepIntvl, sizeof(keepIntvl));
        setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &keepCnt, sizeof(keepCnt));
#endif

        connect(socket, &QTcpSocket::readyRead, this, &HttpServer::readClient, Qt::QueuedConnection);
        connect(socket, &QTcpSocket::disconnected, this, &HttpServer::discardClient, Qt::QueuedConnection);
        connect(socket, QOverload<QAbstractSocket::SocketError>::of(&QTcpSocket::error),
                this, &HttpServer::handleSocketError, Qt::QueuedConnection);
    }
}

void HttpServer::handleSocketError(QAbstractSocket::SocketError) {
    QTcpSocket *socket = qobject_cast<QTcpSocket*>(sender());
    if (!socket) return;
    qWarning() << "[Server] Socket error:" << socket->errorString();
    socket->deleteLater();
}

void HttpServer::readClient() {
    QTcpSocket *socket = qobject_cast<QTcpSocket*>(sender());
    if (!socket) return;
    if (socket->state() != QAbstractSocket::ConnectedState) {
        discardClient();
        return;
    }

    // 处理正在进行的文件上传
    if (uploadFiles_.contains(socket)) {
        UploadContext *ctx = uploadFiles_[socket];
        if (!ctx || !ctx->file || !ctx->file->isOpen()) {
            discardClient();
            return;
        }

        // 一次最多读取 1MB
        QByteArray data = socket->read(1024 * 1024);
        if (data.isEmpty()) {
            if (ctx->received >= ctx->size) {
                finishUpload(socket, ctx);
            } else {
                qWarning() << "[Server] Upload incomplete, socket closed. Received:"
                           << ctx->received << "/" << ctx->size;
                cleanupUploadContext(socket);
                socket->deleteLater();
            }
            return;
        }

        qint64 written = ctx->file->write(data);
        if (written != data.size()) {
            qCritical() << "[Server] File write error: wrote" << written << "of" << data.size()
                        << "error:" << ctx->file->errorString();
            cleanupUploadContext(socket);
            socket->close();
            return;
        }
        ctx->received += written;
        ctx->sinceLastFlush += written;

        // 每累积 1MB 或最后一块时 flush 一次,减少磁盘 I/O
        if (ctx->sinceLastFlush >= FLUSH_INTERVAL_BYTES || ctx->received >= ctx->size) {
            ctx->file->flush();
            ctx->sinceLastFlush = 0;
        }

        qDebug() << "[Server] Upload progress:" << ctx->received << "/" << ctx->size;
        if (ctx->received >= ctx->size) {
            finishUpload(socket, ctx);
        }
        return;
    }

    // 处理新HTTP请求
    QByteArray data = socket->readAll();
    if (data.isEmpty()) {
        socket->close();
        return;
    }
    QByteArray response = parseHttpRequest(data);
    if (response.isEmpty()) {
        response = "HTTP/1.1 500 Internal Server Error\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n{\"status\":\"error\"}";
    }
    socket->write(response);
    socket->waitForBytesWritten(5000);
    socket->flush();
    bool is100 = response.startsWith("HTTP/1.1 100");
    if (is100) {
        if (socket->bytesAvailable() > 0)
            QMetaObject::invokeMethod(this, [this,socket](){ readClient(); }, Qt::QueuedConnection);
        return;
    }
    if (socket->state() == QAbstractSocket::ConnectedState) socket->close();
}

void HttpServer::finishUpload(QTcpSocket *socket, UploadContext *ctx) {
    if (!ctx || !ctx->file) return;
    ctx->file->close();
    QString filePath = ctx->file->fileName();
    QString md5 = calculateMD5(filePath);
    qDebug() << "[Server] Upload finished, MD5:" << md5;
    {
        QMutexLocker locker(&releaseMutex_);
        if (releases_.contains(ctx->type)) {
            ReleaseFile &r = releases_[ctx->type];
            r.filename = ctx->filename;
            r.filepath = filePath;
            r.md5 = md5;
            r.size = ctx->size;
            r.version = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss");
            r.updateTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
            saveReleaseInfo(ctx->type);
        }
    }
    delete ctx->file;
    ctx->file = nullptr;

    // 重置构建任务状态
    {
        QMutexLocker locker(&mutex_);
        if (tasks_.contains(ctx->type)) {
            BuildTask &task = tasks_[ctx->type];
            task.requested = false;
            task.in_progress = false;
            task.completed = true;
            task.status = "idle";
            task.lastBuildTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
            qDebug() << "[Server] Build task for" << ctx->type << "reset to idle";
        }
    }

    QByteArray resp = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n"
                      "{\"status\":\"success\",\"md5\":\"" + md5.toUtf8() + "\",\"size\":" + QByteArray::number(ctx->size) + "}";
    if (socket->state() == QAbstractSocket::ConnectedState) {
        socket->write(resp);
        socket->flush();
        socket->waitForBytesWritten(5000);
        socket->close();
    }
    cleanupUploadContext(socket);
    socket->deleteLater();
}

void HttpServer::cleanupUploadContext(QTcpSocket *socket) {
    if (!uploadFiles_.contains(socket)) return;
    UploadContext *ctx = uploadFiles_.take(socket);
    if (ctx) {
        if (ctx->file) {
            if (ctx->file->isOpen()) {
                ctx->file->close();
                QFile::remove(ctx->file->fileName());
            }
            delete ctx->file;
        }
        delete ctx;
    }
    uploadTypes_.remove(socket); uploadFilenames_.remove(socket); uploadMd5_.remove(socket);
    uploadSize_.remove(socket); uploadReceived_.remove(socket);
}

void HttpServer::discardClient() {
    QTcpSocket *socket = qobject_cast<QTcpSocket*>(sender());
    if (!socket) return;
    qDebug() << "[Server] discardClient for socket" << socket->socketDescriptor();
    cleanupUploadContext(socket);
    socket->deleteLater();
}

QByteArray HttpServer::parseHttpRequest(const QByteArray &data) {
    QStringList lines = QString::fromUtf8(data).split("\r\n");
    if (lines.isEmpty()) return {};
    QStringList reqLine = lines[0].split(" ");
    if (reqLine.size() < 2) return {};
    QString method = reqLine[0], path = reqLine[1];
    QUrl url(path);
    QUrlQuery query(url);
    QString token = query.queryItemValue("token");
    int contentLen = 0;
    for (int i=1; i<lines.size(); ++i) {
        if (lines[i].startsWith("Content-Length:"))
            contentLen = lines[i].mid(16).trimmed().toInt();
    }
    QByteArray body;
    int bodyStart = data.indexOf("\r\n\r\n");
    if (bodyStart != -1) body = data.mid(bodyStart+4, contentLen);
    if (method=="POST" && !body.isEmpty()) {
        QJsonParseError err;
        QJsonDocument doc = QJsonDocument::fromJson(body, &err);
        if (err.error==QJsonParseError::NoError && doc.isObject())
            token = doc.object()["token"].toString();
    }
    if (!authenticate(token))
        return "HTTP/1.1 401 Unauthorized\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n{\"status\":\"unauthorized\"}";
    if (path.contains("/poll")) return handlePoll(query);
    if (path.contains("/upload")) return handleUpload(query, data, qobject_cast<QTcpSocket*>(sender()));
    if (path.contains("/download")) return handleDownload(query);
    if (path.contains("/release")) return handleReleaseInfo(query);
    if (path.contains("/trigger")) { handleTrigger(body); return "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n{\"status\":\"triggered\"}"; }
    if (path.contains("/health")) return handleHealth();
    if (path.contains("/stats")) return handleStats();
    return "HTTP/1.1 404 Not Found\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n{\"status\":\"not_found\"}";
}

QByteArray HttpServer::handlePoll(const QUrlQuery &query) {
    QString agent = query.queryItemValue("agent"), token = query.queryItemValue("token");
    QMutexLocker locker(&mutex_);
    if (!tasks_.contains(agent)) return "HTTP/1.1 404 Not Found\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n{\"status\":\"unknown_agent\"}";
    if (!authenticate(token)) return "HTTP/1.1 401 Unauthorized\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n{\"status\":\"unauthorized\"}";
    BuildTask &t = tasks_[agent];
    if (t.requested && !t.in_progress) {
        t.in_progress = true; t.status = "building";
        QJsonObject resp; resp["status"]="build"; resp["branch"]=t.branch; resp["type"]=t.type; resp["message"]="Start building now";
        return "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n" + QJsonDocument(resp).toJson();
    } else {
        QJsonObject resp; resp["status"]="idle"; resp["message"]="No build requested";
        return "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n" + QJsonDocument(resp).toJson();
    }
}

QByteArray HttpServer::handleUpload(const QUrlQuery &query, const QByteArray &data, QTcpSocket *socket) {
    QString agent = query.queryItemValue("agent"), token = query.queryItemValue("token");
    QMutexLocker locker(&mutex_);
    if (!tasks_.contains(agent)) return "HTTP/1.1 404 Not Found\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n{\"status\":\"unknown_agent\"}";
    if (!authenticate(token)) return "HTTP/1.1 401 Unauthorized\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n{\"status\":\"unauthorized\"}";

    QStringList lines = QString::fromUtf8(data).split("\r\n");
    qint64 fileSize = 0;
    for (int i=1; i<lines.size(); ++i)
        if (lines[i].startsWith("Content-Length:")) { fileSize = lines[i].mid(16).trimmed().toLongLong(); break; }
    QString filename = "artifact.zip";
    for (int i=1; i<lines.size(); ++i)
        if (lines[i].startsWith("X-Filename:")) { filename = lines[i].mid(12).trimmed().replace(QRegExp("[\\\\/:*?\"<>|]"), "_"); break; }

    QString uploadDir = getReleaseDir(agent);
    QDir().mkpath(uploadDir);
    QString filePath = uploadDir + "/" + filename;
    if (QFile::exists(filePath)) QFile::remove(filePath);
    QFile *file = new QFile(filePath);
    if (!file->open(QIODevice::WriteOnly)) { delete file; return "HTTP/1.1 500 Internal Server Error\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n{\"status\":\"file_open_error\"}"; }

    int bodyStart = data.indexOf("\r\n\r\n");
    QByteArray fileData = (bodyStart != -1) ? data.mid(bodyStart+4) : QByteArray();
    if (fileData.size() >= fileSize || fileSize == 0) {
        // 一次性完整上传
        if (file->write(fileData) != fileData.size()) {
            file->close(); delete file; QFile::remove(filePath);
            return "HTTP/1.1 500\r\n\r\n";
        }
        file->close();
        QString md5 = calculateMD5(filePath);
        {
            QMutexLocker l(&releaseMutex_);
            if (releases_.contains(agent)) {
                ReleaseFile &r = releases_[agent];
                r.filename = filename; r.filepath = filePath; r.md5 = md5; r.size = fileData.size();
                r.version = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss");
                r.updateTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
                saveReleaseInfo(agent);
            }
        }
        delete file;

        // 重置构建任务状态
        if (tasks_.contains(agent)) {
            BuildTask &task = tasks_[agent];
            task.requested = false;
            task.in_progress = false;
            task.completed = true;
            task.status = "idle";
            task.lastBuildTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
            qDebug() << "[Server] Build task for" << agent << "reset to idle (one-shot upload)";
        }

        return "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n{\"status\":\"success\",\"md5\":\"" + md5.toUtf8() + "\",\"size\":" + QByteArray::number(fileData.size()) + "}";
    }

    // 分块上传:写入已接收部分,返回 100 Continue
    file->write(fileData);
    file->flush();
    UploadContext *ctx = new UploadContext;
    ctx->file = file;
    ctx->type = agent;
    ctx->filename = filename;
    ctx->size = fileSize;
    ctx->received = fileData.size();
    ctx->sinceLastFlush = fileData.size(); // 初始数据已 flush
    uploadFiles_[socket] = ctx;
    return "HTTP/1.1 100 Continue\r\nConnection: keep-alive\r\n\r\n";
}

void HttpServer::handleTrigger(const QByteArray &body) {
    QJsonParseError err;
    QJsonDocument doc = QJsonDocument::fromJson(body, &err);
    if (err.error != QJsonParseError::NoError || !doc.isObject()) return;
    QJsonObject obj = doc.object();
    QString type = obj["type"].toString();
    QString branch = obj["branch"].toString("main");
    QString token = obj["token"].toString();
    if (!authenticate(token)) return;
    if (type.isEmpty()) return;
    triggerBuild(type, branch);
}

QByteArray HttpServer::handleDownload(const QUrlQuery &query) {
    QString agent = query.queryItemValue("agent"), token = query.queryItemValue("token");
    QMutexLocker locker(&releaseMutex_);
    if (!releases_.contains(agent)) return "HTTP/1.1 404\r\n\r\n";
    if (!authenticate(token)) return "HTTP/1.1 401\r\n\r\n";
    const ReleaseFile &r = releases_[agent];
    if (r.filename.isEmpty()) return "HTTP/1.1 404\r\n\r\n";
    QFile file(r.filepath);
    if (!file.open(QIODevice::ReadOnly)) return "HTTP/1.1 500\r\n\r\n";
    QByteArray resp = "HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\nContent-Disposition: attachment; filename=\"" + r.filename.toUtf8() + "\"\r\nContent-Length: " + QByteArray::number(r.size) + "\r\nX-MD5: " + r.md5.toUtf8() + "\r\nConnection: close\r\n\r\n";
    QTcpSocket *socket = qobject_cast<QTcpSocket*>(sender());
    if (socket) {
        socket->write(resp);
        socket->waitForBytesWritten(1000);
        // 使用 256KB 块发送,减少系统调用
        while (!file.atEnd()) {
            QByteArray chunk = file.read(DOWNLOAD_CHUNK_SIZE);
            if (chunk.isEmpty()) break;
            qint64 sent = socket->write(chunk);
            if (sent != chunk.size()) {
                qWarning() << "[Server] Failed to send all data during download";
                break;
            }
            socket->waitForBytesWritten(1000);
        }
        file.close();
        socket->close();
    }
    return {};
}

QByteArray HttpServer::handleReleaseInfo(const QUrlQuery &query) {
    QString agent = query.queryItemValue("agent"), token = query.queryItemValue("token");
    if (!authenticate(token)) return "HTTP/1.1 401\r\n\r\n";
    return "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n" + getReleaseInfo(agent);
}

QByteArray HttpServer::handleHealth() {
    QJsonObject obj; obj["status"]="ok"; obj["timestamp"]=QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
    return "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n" + QJsonDocument(obj).toJson();
}

QByteArray HttpServer::handleStats() {
    return "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n" + getAllStatus();
}

(4)main.cpp

复制代码
#include <QCoreApplication>
#include <QCommandLineParser>
#include <QCommandLineOption>
#include <QDir>
#include <QDebug>
#include "http_server.h"

#ifdef Q_OS_UNIX
#include <signal.h>
#include <sys/resource.h>
#endif

#ifdef Q_OS_WIN
#include <windows.h>
#endif

static QCoreApplication *gApp = nullptr;

#ifdef Q_OS_UNIX
void signalHandler(int sig) {
    if (gApp) {
        qDebug() << "Received signal" << sig << ", shutting down...";
        gApp->quit();
    }
}

void setMaxFileDescriptors() {
    struct rlimit rl;
    if (getrlimit(RLIMIT_NOFILE, &rl) == 0) {
        rlim_t newMax = rl.rlim_max;
        if (rl.rlim_cur < newMax) {
            rl.rlim_cur = newMax;
            if (setrlimit(RLIMIT_NOFILE, &rl) == 0) {
                qDebug() << "[Main] File descriptor limit increased to" << newMax;
            } else {
                qWarning() << "[Main] Failed to increase file descriptor limit";
            }
        } else {
            qDebug() << "[Main] File descriptor limit already at" << rl.rlim_cur;
        }
    } else {
        qWarning() << "[Main] Failed to get file descriptor limit";
    }
}
#endif

#ifdef Q_OS_WIN
BOOL WINAPI consoleHandler(DWORD dwType) {
    if (dwType == CTRL_C_EVENT || dwType == CTRL_CLOSE_EVENT) {
        if (gApp) {
            qDebug() << "Received shutdown event, quitting...";
            QMetaObject::invokeMethod(gApp, &QCoreApplication::quit, Qt::QueuedConnection);
        }
        return TRUE;
    }
    return FALSE;
}
#endif

int main(int argc, char *argv[]) {
    QCoreApplication app(argc, argv);
    gApp = &app;

    app.setApplicationName("BuildServer");
    app.setApplicationVersion("1.0");

#ifdef Q_OS_UNIX
    // 提高文件描述符限制(仅 Unix)
    setMaxFileDescriptors();

    // 注册信号处理(仅 Unix)
    signal(SIGINT, signalHandler);
    signal(SIGTERM, signalHandler);
    signal(SIGHUP, signalHandler);
#endif

#ifdef Q_OS_WIN
    // Windows 控制台事件处理
    SetConsoleCtrlHandler(consoleHandler, TRUE);
#endif

    QCommandLineParser parser;
    parser.setApplicationDescription("Build Server - Central build management");
    parser.addHelpOption();
    parser.addVersionOption();

    QCommandLineOption portOption(QStringList() << "p" << "port",
                                  "Server port", "port", "8080");
    parser.addOption(portOption);

    parser.process(app);

    quint16 port = parser.value(portOption).toUShort();

    HttpServer server(port);
    server.start();

    QObject::connect(&app, &QCoreApplication::aboutToQuit, [&server]() {
        server.stop();
    });

    qDebug() << "========================================";
    qDebug() << "   Build Server (Cross-Platform)";
    qDebug() << "========================================";
    qDebug() << "Port:" << port;
    qDebug() << "Press Ctrl+C to stop";
    qDebug() << "========================================";

    return app.exec();
}
2、客户端

(1)build_client.pro

复制代码
# build_client.pro

QT += core network
QT -= gui

TARGET = BuildClient
TEMPLATE = app

CONFIG += c++17 console automoc
CONFIG -= qt_quickcompiler

VERSION = 1.0.0
DEFINES += VERSION=\\\"$${VERSION}\\\"

# 编译优化
CONFIG += release
CONFIG += warn_on

unix {
    DEFINES += Q_OS_LINUX
    QMAKE_CXXFLAGS += -Wall -Wextra -Wpedantic -O2
    QMAKE_LFLAGS += -s  # Strip symbols
    LIBS += -lpthread
}

win32 {
    DEFINES += Q_OS_WIN
    QMAKE_CXXFLAGS += /MP /O2 /W3
    QMAKE_LFLAGS += /LTCG  # Link Time Code Generation
}

macx {
    DEFINES += Q_OS_MAC
    QMAKE_CXXFLAGS += -Wall -Wextra -O2
}

INCLUDEPATH += $$PWD/include

SOURCES += \
    src/main.cpp \
    src/build_client.cpp

HEADERS += \
    include/build_client.h

DESTDIR = $$PWD/bin
OBJECTS_DIR = $$PWD/obj
MOC_DIR = $$PWD/moc

QMAKE_CLEAN += bin/* obj/* moc/*

# 安装配置
unix:!macx {
    target.path = /usr/local/bin
    INSTALLS += target
}

# Windows 资源文件
win32 {
    RC_ICONS = $$PWD/resources/icon.ico
}

# 打印编译信息
message("Building BuildClient v$${VERSION}")
message("Target: $$TARGET")
message("Output directory: $$DESTDIR")

(2)build_client.h

复制代码
#ifndef BUILD_CLIENT_H
#define BUILD_CLIENT_H

#include <QObject>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QUrl>
#include <QUrlQuery>
#include <QTimer>
#include <QProcess>
#include <QFile>
#include <QDir>
#include <QStandardPaths>
#include <QDateTime>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QSettings>
#include <QThread>
#include <QMutex>
#include <QReadWriteLock>
#include <QElapsedTimer>
#include <QDebug>
#include <QCryptographicHash>
#include <memory>
#include <functional>

// 客户端状态枚举
enum class ClientState {
    Idle,
    Polling,
    Building,
    Uploading,
    Downloading,
    Stopped
};

// 构建结果枚举
enum class BuildResult {
    Success,
    Failed,
    Cancelled
};

class BuildClient : public QObject {
    Q_OBJECT

public:
    explicit BuildClient(const QString &serverUrl, const QString &agentType,
                        const QString &token, QObject *parent = nullptr);
    ~BuildClient();

    // 生命周期管理
    void start();
    void stop();
    bool isRunning() const;
    ClientState state() const;

    // 配置方法
    void setBuildScript(const QString &scriptPath);
    void setOutputDir(const QString &outputDir);
    void setPollInterval(int milliseconds);
    void setRequestTimeout(int milliseconds);
    void setMaxRetries(int retries);

    // 统计信息
    int getSuccessCount() const;
    int getFailCount() const;
    QString getLastError() const;
    
    // 信号用于监控
signals:
    void stateChanged(ClientState newState, ClientState oldState);
    void buildStarted(const QString &branch);
    void buildCompleted(bool success, int exitCode);
    void uploadProgress(qint64 bytesSent, qint64 totalBytes);
    void downloadProgress(qint64 bytesReceived, qint64 totalBytes);
    void statusUpdated(const QString &status, const QString &message);
    void errorOccurred(const QString &error, int errorCode);

private slots:
    void pollServer();
    void onPollReply(QNetworkReply *reply);
    void onUploadReply(QNetworkReply *reply);
    void onDownloadReply(QNetworkReply *reply);
    void onReleaseInfoReply(QNetworkReply *reply);
    void onBuildProcessFinished(int exitCode, QProcess::ExitStatus exitStatus);
    void onBuildProcessError(QProcess::ProcessError error);
    void onNetworkError(QNetworkReply::NetworkError code);
    void retryPoll();

private:
    // 核心功能
    void executeBuildScript(const QString &branch);
    void uploadArtifact(const QString &filePath);
    void downloadArtifact(const QString &version = QString());
    void checkForUpdate();
    
    // 辅助方法
    QString createZipFile(const QString &sourceDir);
    QString createTemporaryDirectory() const;
    void cleanupTemporaryFiles();
    bool verifyMD5(const QString &filePath, const QString &expectedMd5) const;
    void saveBuildLog(const QString &message) const;
    void updateState(ClientState newState);
    void updateStatus(const QString &status, const QString &message = QString());
    
    // 网络请求辅助方法
    QNetworkReply* sendGetRequest(const QString &endpoint, const QUrlQuery &extraQuery = QUrlQuery());
    QNetworkReply* sendPostRequest(const QString &endpoint, const QByteArray &data, 
                                   const QMap<QByteArray, QByteArray> &headers = QMap<QByteArray, QByteArray>());
    QNetworkReply* sendFileUpload(const QString &endpoint, const QString &filePath, 
                                  const QMap<QByteArray, QByteArray> &extraHeaders = QMap<QByteArray, QByteArray>());
    
    // 错误处理
    void handleNetworkError(QNetworkReply *reply, const QString &context);
    void scheduleRetry(const std::function<void()> &operation, const QString &operationName);

    // 成员变量
    QString serverUrl_;
    QString agentType_;
    QString token_;
    QString buildScript_;
    QString outputDir_;
    QString tempDir_;
    
    // 网络组件
    std::unique_ptr<QNetworkAccessManager> networkManager_;
    std::unique_ptr<QTimer> pollTimer_;
    std::unique_ptr<QTimer> retryTimer_;
    std::unique_ptr<QProcess> buildProcess_;
    
    // 状态管理
    mutable QReadWriteLock stateLock_;
    ClientState currentState_;
    bool building_;
    int retryCount_;
    int maxRetries_;
    
    // 统计信息
    int successCount_;
    int failCount_;
    QString lastError_;
    
    // 配置参数
    int pollIntervalMs_;
    int requestTimeoutMs_;
    
    // 当前构建信息
    QString currentBranch_;
    QString currentArtifactPath_;
    QString currentVersion_;
    QString expectedMd5_;
    
    // 时间统计
    QElapsedTimer buildTimer_;
    
    // 常量定义
    static constexpr int DEFAULT_POLL_INTERVAL_MS = 3000;
    static constexpr int DEFAULT_REQUEST_TIMEOUT_MS = 30000;
    static constexpr int DEFAULT_MAX_RETRIES = 3;
    static constexpr int RETRY_DELAY_MS = 5000;
    static constexpr int CHUNK_SIZE = 8192;
    static constexpr int PROCESS_TIMEOUT_MS = 300000; // 5 minutes
    static constexpr int WAIT_FOR_FINISHED_MS = 5000;
};

#endif

(3)build_client.cpp

复制代码
#include "build_client.h"
#include <QCoreApplication>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QNetworkAccessManager>
#include <QFile>
#include <QDir>
#include <QStandardPaths>
#include <QDateTime>
#include <QProcess>
#include <QSettings>
#include <QUrlQuery>
#include <QCryptographicHash>
#include <QTemporaryDir>
#include <QThread>
#include <QTimer>
#include <QDebug>

// 辅助宏
#define LOCK_READ(mutex) QReadLocker locker(&mutex)
#define LOCK_WRITE(mutex) QWriteLocker locker(&mutex)

BuildClient::BuildClient(const QString &serverUrl, const QString &agentType,
                         const QString &token, QObject *parent)
    : QObject(parent)
    , serverUrl_(serverUrl)
    , agentType_(agentType)
    , token_(token)
    , networkManager_(std::make_unique<QNetworkAccessManager>(this))
    , pollTimer_(std::make_unique<QTimer>(this))
    , retryTimer_(std::make_unique<QTimer>(this))
    , buildProcess_(std::make_unique<QProcess>(this))
    , currentState_(ClientState::Idle)
    , building_(false)
    , retryCount_(0)
    , maxRetries_(DEFAULT_MAX_RETRIES)
    , successCount_(0)
    , failCount_(0)
    , pollIntervalMs_(DEFAULT_POLL_INTERVAL_MS)
    , requestTimeoutMs_(DEFAULT_REQUEST_TIMEOUT_MS)
{
    // 初始化路径
    buildScript_ = QCoreApplication::applicationDirPath() + QDir::separator() + "build.sh";
    outputDir_ = QCoreApplication::applicationDirPath() + QDir::separator() + "build_output";
    tempDir_ = QDir::tempPath() + QDir::separator() + "build_client_" + 
               QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss");

    // 创建必要的目录
    QDir dir(outputDir_);
    if (!dir.exists()) {
        dir.mkpath(".");
    }
    
    QDir tempDir(tempDir_);
    if (!tempDir.exists()) {
        tempDir.mkpath(".");
    }
    
    // 配置定时器
    pollTimer_->setInterval(pollIntervalMs_);
    pollTimer_->setSingleShot(false);
    retryTimer_->setSingleShot(true);
    
    // 连接信号槽
    connect(pollTimer_.get(), &QTimer::timeout, this, &BuildClient::pollServer);
    connect(retryTimer_.get(), &QTimer::timeout, this, &BuildClient::retryPoll);
    
    // 处理网络错误
    connect(networkManager_.get(), &QNetworkAccessManager::finished,
            this, [this](QNetworkReply *reply) {
                if (reply->error() != QNetworkReply::NoError) {
                    handleNetworkError(reply, reply->url().toString());
                }
                
                QString url = reply->url().toString();
                if (url.contains("/poll")) {
                    onPollReply(reply);
                } else if (url.contains("/upload")) {
                    onUploadReply(reply);
                } else if (url.contains("/download")) {
                    onDownloadReply(reply);
                } else if (url.contains("/release")) {
                    onReleaseInfoReply(reply);
                }
                reply->deleteLater();
            });
    
    // 构建进程信号
    connect(buildProcess_.get(), QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
            this, &BuildClient::onBuildProcessFinished);
    connect(buildProcess_.get(), QOverload<QProcess::ProcessError>::of(&QProcess::errorOccurred),
            this, &BuildClient::onBuildProcessError);
    
    qDebug() << "[Client] Initialized - Agent:" << agentType_ << "Server:" << serverUrl_;
}

BuildClient::~BuildClient() {
    stop();
    cleanupTemporaryFiles();
}

void BuildClient::start() {
    if (isRunning()) return;
    
    updateState(ClientState::Polling);
    pollTimer_->start();
    retryCount_ = 0;
    
    qDebug() << "[Client] Started - Polling every" << pollIntervalMs_ << "ms";
    qDebug() << "[Client] Server URL:" << serverUrl_;
    qDebug() << "[Client] Agent Type:" << agentType_;
    qDebug() << "[Client] Output Directory:" << outputDir_;
    
    updateStatus("started", "Client started successfully");
}

void BuildClient::stop() {
    if (!isRunning()) return;
    
    updateState(ClientState::Stopped);
    pollTimer_->stop();
    retryTimer_->stop();
    
    if (buildProcess_ && buildProcess_->state() != QProcess::NotRunning) {
        buildProcess_->terminate();
        buildProcess_->waitForFinished(WAIT_FOR_FINISHED_MS);
        if (buildProcess_->state() != QProcess::NotRunning) {
            buildProcess_->kill();
            buildProcess_->waitForFinished(WAIT_FOR_FINISHED_MS);
        }
    }
    
    building_ = false;
    
    qDebug() << "[Client] Stopped - Success:" << successCount_ << ", Failed:" << failCount_;
    updateStatus("stopped", QString("Success: %1, Failed: %2").arg(successCount_).arg(failCount_));
}

bool BuildClient::isRunning() const {
    LOCK_READ(stateLock_);
    return currentState_ != ClientState::Stopped && pollTimer_->isActive();
}

ClientState BuildClient::state() const {
    LOCK_READ(stateLock_);
    return currentState_;
}

void BuildClient::setBuildScript(const QString &scriptPath) {
    buildScript_ = scriptPath;
    qDebug() << "[Client] Build script set to:" << buildScript_;
}

void BuildClient::setOutputDir(const QString &outputDir) {
    outputDir_ = outputDir;
    QDir dir(outputDir_);
    if (!dir.exists()) {
        dir.mkpath(".");
    }
    qDebug() << "[Client] Output directory set to:" << outputDir_;
}

void BuildClient::setPollInterval(int milliseconds) {
    if (milliseconds >= 1000) {
        pollIntervalMs_ = milliseconds;
        if (pollTimer_->isActive()) {
            pollTimer_->setInterval(pollIntervalMs_);
        }
        qDebug() << "[Client] Poll interval set to:" << pollIntervalMs_ << "ms";
    }
}

void BuildClient::setRequestTimeout(int milliseconds) {
    requestTimeoutMs_ = milliseconds;
    qDebug() << "[Client] Request timeout set to:" << requestTimeoutMs_ << "ms";
}

void BuildClient::setMaxRetries(int retries) {
    maxRetries_ = qMax(0, retries);
    qDebug() << "[Client] Max retries set to:" << maxRetries_;
}

int BuildClient::getSuccessCount() const {
    return successCount_;
}

int BuildClient::getFailCount() const {
    return failCount_;
}

QString BuildClient::getLastError() const {
    return lastError_;
}

void BuildClient::pollServer() {
    if (building_ || currentState_ == ClientState::Stopped) {
        return;
    }
    
    QUrlQuery query;
    query.addQueryItem("agent", agentType_);
    query.addQueryItem("token", token_);
    
    sendGetRequest("/poll", query);
}

void BuildClient::onPollReply(QNetworkReply *reply) {
    if (reply->error() != QNetworkReply::NoError) {
        return; // 错误已在 handleNetworkError 中处理
    }
    
    retryCount_ = 0; // 重置重试计数
    
    QByteArray response = reply->readAll();
    qDebug() << "[Client] Poll response:" << response.left(200);
    
    QJsonParseError parseError;
    QJsonDocument doc = QJsonDocument::fromJson(response, &parseError);
    
    if (parseError.error != QJsonParseError::NoError) {
        qWarning() << "[Client] JSON parse error:" << parseError.errorString();
        return;
    }
    
    QJsonObject obj = doc.object();
    QString status = obj["status"].toString();
    
    if (status == "build") {
        currentBranch_ = obj["branch"].toString("main");
        qDebug() << "[Client] Build requested for branch:" << currentBranch_;
        executeBuildScript(currentBranch_);
    } else {
        // 空闲时检查更新
        if (!building_) {
            checkForUpdate();
        }
    }
}

void BuildClient::onUploadReply(QNetworkReply *reply) {
    updateState(ClientState::Idle);
    building_ = false;
    
    if (reply->error() != QNetworkReply::NoError) {
        failCount_++;
        updateStatus("upload_failed", reply->errorString());
        return;
    }
    
    QByteArray response = reply->readAll();
    QJsonParseError parseError;
    QJsonDocument doc = QJsonDocument::fromJson(response, &parseError);
    
    if (parseError.error == QJsonParseError::NoError) {
        QJsonObject obj = doc.object();
        QString status = obj["status"].toString();
        
        if (status == "success") {
            successCount_++;
            qDebug() << "[Client] Upload successful - MD5:" << obj["md5"].toString();
            updateStatus("upload_success", "Artifact uploaded successfully");
            emit uploadProgress(reply->request().header(QNetworkRequest::ContentLengthHeader).toLongLong(),
                              reply->request().header(QNetworkRequest::ContentLengthHeader).toLongLong());
        } else {
            failCount_++;
            QString message = obj["message"].toString();
            qWarning() << "[Client] Upload failed:" << message;
            updateStatus("upload_failed", message);
        }
    }
    
    // 清理临时文件
    if (!currentArtifactPath_.isEmpty() && QFile::exists(currentArtifactPath_)) {
        QFile::remove(currentArtifactPath_);
    }
}

void BuildClient::onDownloadReply(QNetworkReply *reply) {
    updateState(ClientState::Idle);
    
    if (reply->error() != QNetworkReply::NoError) {
        failCount_++;
        updateStatus("download_failed", reply->errorString());
        return;
    }
    
    // 获取 MD5
    QString expectedMd5 = QString::fromUtf8(reply->rawHeader("X-MD5"));
    
    // 保存文件
    QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss");
    QString filename = QString("download_%1_%2.zip").arg(agentType_).arg(timestamp);
    QString filepath = outputDir_ + "/" + filename;
    
    QFile file(filepath);
    if (file.open(QIODevice::WriteOnly)) {
        QByteArray data = reply->readAll();
        file.write(data);
        file.close();
        
        qDebug() << "[Client] Downloaded" << data.size() << "bytes to" << filepath;
        
        // 验证 MD5
        if (!expectedMd5.isEmpty()) {
            if (verifyMD5(filepath, expectedMd5)) {
                qDebug() << "[Client] Download successful, MD5 verified:" << expectedMd5;
                successCount_++;
                updateStatus("download_success", "File downloaded and verified");
                emit downloadProgress(data.size(), data.size());
            } else {
                qWarning() << "[Client] MD5 verification failed! Expected:" << expectedMd5;
                failCount_++;
                updateStatus("md5_failed", "MD5 verification failed");
                QFile::remove(filepath);
            }
        } else {
            qDebug() << "[Client] Download successful (no MD5 verification)";
            successCount_++;
            updateStatus("download_success", "File downloaded");
        }
    } else {
        qWarning() << "[Client] Failed to save downloaded file";
        failCount_++;
        updateStatus("save_failed", "Cannot save file");
    }
}

void BuildClient::onReleaseInfoReply(QNetworkReply *reply) {
    if (reply->error() != QNetworkReply::NoError) {
        return;
    }
    
    QByteArray response = reply->readAll();
    QJsonParseError parseError;
    QJsonDocument doc = QJsonDocument::fromJson(response, &parseError);
    
    if (parseError.error != QJsonParseError::NoError) {
        return;
    }
    
    QJsonObject obj = doc.object();
    QString version = obj["version"].toString();
    QString md5 = obj["md5"].toString();
    
    // 检查是否有新版本
    if (version != lastVersion_ || md5 != lastMd5_) {
        qDebug() << "[Client] New version available:" << version;
        lastVersion_ = version;
        lastMd5_ = md5;
        downloadArtifact(version);
    }
}

void BuildClient::onBuildProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) {
    qint64 buildTimeMs = buildTimer_.elapsed();
    
    if (exitCode == 0 && exitStatus == QProcess::NormalExit) {
        qDebug() << "[Client] Build completed successfully in" << (buildTimeMs / 1000.0) << "seconds";
        updateStatus("build_success", QString("Completed in %1 ms").arg(buildTimeMs));
        
        QString zipFile = createZipFile(outputDir_);
        if (!zipFile.isEmpty()) {
            currentArtifactPath_ = zipFile;
            uploadArtifact(zipFile);
        } else {
            failCount_++;
            updateStatus("zip_failed", "Failed to create archive");
            building_ = false;
            updateState(ClientState::Polling);
        }
    } else {
        qCritical() << "[Client] Build failed with exit code:" << exitCode;
        failCount_++;
        updateStatus("build_failed", QString("Exit code: %1").arg(exitCode));
        building_ = false;
        updateState(ClientState::Polling);
    }
    
    emit buildCompleted(exitCode == 0, exitCode);
}

void BuildClient::onBuildProcessError(QProcess::ProcessError error) {
    QString errorMsg;
    switch (error) {
        case QProcess::FailedToStart:
            errorMsg = "Failed to start build process";
            break;
        case QProcess::Crashed:
            errorMsg = "Build process crashed";
            break;
        case QProcess::Timedout:
            errorMsg = "Build process timed out";
            break;
        default:
            errorMsg = "Unknown build process error";
    }
    
    qCritical() << "[Client] Build error:" << errorMsg;
    failCount_++;
    updateStatus("build_error", errorMsg);
    building_ = false;
    updateState(ClientState::Polling);
    
    emit errorOccurred(errorMsg, static_cast<int>(error));
}

void BuildClient::onNetworkError(QNetworkReply::NetworkError code) {
    QString errorMsg = QNetworkReply::errorString(code);
    qWarning() << "[Client] Network error:" << errorMsg << "(code:" << code << ")";
    emit errorOccurred(errorMsg, code);
}

void BuildClient::retryPoll() {
    if (!building_ && currentState_ != ClientState::Stopped) {
        pollServer();
    }
}

void BuildClient::executeBuildScript(const QString &branch) {
    if (building_) {
        qWarning() << "[Client] Already building, skipping";
        return;
    }
    
    building_ = true;
    currentBranch_ = branch;
    updateState(ClientState::Building);
    updateStatus("building", "Build started for branch: " + branch);
    emit buildStarted(branch);
    
    // 清理输出目录
    QDir outputDir(outputDir_);
    outputDir.removeRecursively();
    outputDir.mkpath(".");
    
    // 检查构建脚本
    if (!QFile::exists(buildScript_)) {
        qCritical() << "[Client] Build script not found:" << buildScript_;
        building_ = false;
        failCount_++;
        updateStatus("script_not_found", buildScript_);
        updateState(ClientState::Polling);
        return;
    }
    
    // 准备构建参数
    QStringList arguments;
#ifdef Q_OS_WIN
    arguments << "/C" << buildScript_;
    buildProcess_->start("cmd.exe", arguments);
#else
    // 确保脚本可执行
    QFile::setPermissions(buildScript_, 
                          QFile::permissions(buildScript_) | QFile::ExeOwner | QFile::ExeUser);
    arguments << buildScript_;
    buildProcess_->start("bash", arguments);
#endif
    
    // 设置环境变量
    QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
    env.insert("BUILD_BRANCH", branch);
    env.insert("BUILD_AGENT_TYPE", agentType_);
    env.insert("BUILD_OUTPUT_DIR", outputDir_);
    buildProcess_->setProcessEnvironment(env);
    buildProcess_->setWorkingDirectory(QFileInfo(buildScript_).absolutePath());
    
    if (!buildProcess_->waitForStarted(WAIT_FOR_FINISHED_MS)) {
        qCritical() << "[Client] Failed to start build process:" << buildProcess_->errorString();
        building_ = false;
        failCount_++;
        updateStatus("start_failed", buildProcess_->errorString());
        updateState(ClientState::Polling);
        return;
    }
    
    buildTimer_.start();
    saveBuildLog("Build started - Branch: " + branch + ", Script: " + buildScript_);
    
    // 设置超时
    QTimer::singleShot(PROCESS_TIMEOUT_MS, this, [this]() {
        if (building_ && buildProcess_->state() != QProcess::NotRunning) {
            qWarning() << "[Client] Build process timed out, terminating...";
            buildProcess_->terminate();
        }
    });
}

void BuildClient::uploadArtifact(const QString &filePath) {
    if (!QFile::exists(filePath)) {
        qWarning() << "[Client] Artifact not found:" << filePath;
        failCount_++;
        updateStatus("artifact_not_found", filePath);
        building_ = false;
        updateState(ClientState::Polling);
        return;
    }
    
    updateState(ClientState::Uploading);
    updateStatus("uploading", "Uploading artifact: " + QFileInfo(filePath).fileName());
    
    QFileInfo fileInfo(filePath);
    QMap<QByteArray, QByteArray> headers;
    headers["X-Agent-Type"] = agentType_.toUtf8();
    headers["X-Filename"] = fileInfo.fileName().toUtf8();
    
    sendFileUpload("/upload", filePath, headers);
}

void BuildClient::downloadArtifact(const QString &version) {
    if (building_) {
        qDebug() << "[Client] Build in progress, skipping download";
        return;
    }
    
    updateState(ClientState::Downloading);
    updateStatus("downloading", "Downloading version: " + (version.isEmpty() ? "latest" : version));
    
    QUrlQuery query;
    query.addQueryItem("agent", agentType_);
    query.addQueryItem("token", token_);
    if (!version.isEmpty()) {
        query.addQueryItem("version", version);
    }
    
    sendGetRequest("/download", query);
}

void BuildClient::checkForUpdate() {
    QUrlQuery query;
    query.addQueryItem("agent", agentType_);
    query.addQueryItem("token", token_);
    
    sendGetRequest("/release", query);
}

QString BuildClient::createZipFile(const QString &sourceDir) {
    QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss");
    QString zipName = QString("artifact_%1_%2.zip").arg(agentType_).arg(timestamp);
    QString zipPath = tempDir_ + "/" + zipName;
    
    qDebug() << "[Client] Creating zip archive:" << zipPath;
    
    QDir sourceDirObj(sourceDir);
    QStringList files = sourceDirObj.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
    
    if (files.isEmpty()) {
        qWarning() << "[Client] No files to archive in:" << sourceDir;
        return QString();
    }
    
    bool success = false;
    
#ifdef Q_OS_WIN
    QProcess zipProcess;
    QStringList args;
    args << "-Command" 
         << QString("Compress-Archive -Path '%1\\*' -DestinationPath '%2' -Force")
               .arg(sourceDir).arg(zipPath);
    zipProcess.start("powershell.exe", args);
    success = zipProcess.waitForFinished(PROCESS_TIMEOUT_MS) && zipProcess.exitCode() == 0;
#else
    QProcess zipProcess;
    zipProcess.setWorkingDirectory(sourceDir);
    zipProcess.start("zip", {"-r", zipPath, "."});
    success = zipProcess.waitForFinished(PROCESS_TIMEOUT_MS) && zipProcess.exitCode() == 0;
#endif
    
    if (success && QFile::exists(zipPath)) {
        qint64 fileSize = QFileInfo(zipPath).size();
        qDebug() << "[Client] Zip archive created:" << zipPath << "(" << fileSize << "bytes)";
        return zipPath;
    } else {
        qWarning() << "[Client] Failed to create zip archive";
        return QString();
    }
}

QString BuildClient::createTemporaryDirectory() const {
    QString tempPath = QDir::tempPath() + QDir::separator() + 
                       "build_client_" + QUuid::createUuid().toString(QUuid::WithoutBraces);
    QDir().mkpath(tempPath);
    return tempPath;
}

void BuildClient::cleanupTemporaryFiles() {
    if (!tempDir_.isEmpty() && QDir(tempDir_).exists()) {
        QDir(tempDir_).removeRecursively();
        qDebug() << "[Client] Cleaned up temporary directory:" << tempDir_;
    }
}

bool BuildClient::verifyMD5(const QString &filePath, const QString &expectedMd5) const {
    QFile file(filePath);
    if (!file.open(QIODevice::ReadOnly)) return false;
    
    QCryptographicHash hash(QCryptographicHash::Md5);
    QByteArray buffer;
    buffer.resize(CHUNK_SIZE);
    
    while (!file.atEnd()) {
        qint64 bytesRead = file.read(buffer.data(), CHUNK_SIZE);
        if (bytesRead > 0) {
            hash.addData(buffer.data(), bytesRead);
        }
    }
    file.close();
    
    QString actualMd5 = hash.result().toHex();
    return actualMd5.compare(expectedMd5, Qt::CaseInsensitive) == 0;
}

void BuildClient::saveBuildLog(const QString &message) const {
    QString logPath = outputDir_ + "/build.log";
    QFile file(logPath);
    
    if (file.open(QIODevice::Append | QIODevice::Text)) {
        QString timestamp = QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss.zzz");
        QString logEntry = QString("[%1] %2\n").arg(timestamp).arg(message);
        file.write(logEntry.toUtf8());
        file.close();
    }
}

void BuildClient::updateState(ClientState newState) {
    LOCK_WRITE(stateLock_);
    if (currentState_ != newState) {
        ClientState oldState = currentState_;
        currentState_ = newState;
        emit stateChanged(newState, oldState);
        qDebug() << "[Client] State changed:" << static_cast<int>(oldState) 
                 << "->" << static_cast<int>(newState);
    }
}

void BuildClient::updateStatus(const QString &status, const QString &message) {
    QString fullMessage = message.isEmpty() ? status : message;
    qDebug() << "[Client] Status:" << status << "-" << fullMessage;
    emit statusUpdated(status, fullMessage);
    saveBuildLog("Status: " + status + " - " + fullMessage);
}

void BuildClient::handleNetworkError(QNetworkReply *reply, const QString &context) {
    QNetworkReply::NetworkError error = reply->error();
    QString errorString = reply->errorString();
    
    qWarning() << "[Client] Network error in" << context << ":" << errorString << "(code:" << error << ")";
    lastError_ = errorString;
    
    // 可重试的错误
    bool retryable = (error == QNetworkReply::TimeoutError ||
                      error == QNetworkReply::ConnectionRefusedError ||
                      error == QNetworkReply::HostNotFoundError ||
                      error == QNetworkReply::TemporaryNetworkFailureError);
    
    if (retryable && retryCount_ < maxRetries_) {
        retryCount_++;
        qDebug() << "[Client] Retrying... (" << retryCount_ << "/" << maxRetries_ << ")";
        retryTimer_->start(RETRY_DELAY_MS);
    } else {
        retryCount_ = 0;
        if (currentState_ == ClientState::Polling) {
            // 保持轮询状态,继续尝试
            pollTimer_->start();
        }
    }
    
    emit errorOccurred(errorString, error);
}

void BuildClient::scheduleRetry(const std::function<void()> &operation, const QString &operationName) {
    if (retryCount_ < maxRetries_) {
        retryCount_++;
        qDebug() << "[Client] Scheduling retry for" << operationName 
                 << "(" << retryCount_ << "/" << maxRetries_ << ")";
        QTimer::singleShot(RETRY_DELAY_MS, this, [this, operation]() {
            operation();
        });
    } else {
        qWarning() << "[Client] Max retries reached for" << operationName;
        retryCount_ = 0;
    }
}

QNetworkReply* BuildClient::sendGetRequest(const QString &endpoint, const QUrlQuery &extraQuery) {
    QUrl url(serverUrl_ + endpoint);
    QUrlQuery query = extraQuery;
    url.setQuery(query);
    
    QNetworkRequest request(url);
    request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
    request.setTransferTimeout(requestTimeoutMs_);
    
    return networkManager_->get(request);
}

QNetworkReply* BuildClient::sendPostRequest(const QString &endpoint, const QByteArray &data,
                                            const QMap<QByteArray, QByteArray> &headers) {
    QUrl url(serverUrl_ + endpoint);
    QNetworkRequest request(url);
    request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
    request.setTransferTimeout(requestTimeoutMs_);
    
    for (auto it = headers.begin(); it != headers.end(); ++it) {
        request.setRawHeader(it.key(), it.value());
    }
    
    return networkManager_->post(request, data);
}

QNetworkReply* BuildClient::sendFileUpload(const QString &endpoint, const QString &filePath,
                                           const QMap<QByteArray, QByteArray> &extraHeaders) {
    QUrl url(serverUrl_ + endpoint);
    QUrlQuery query;
    query.addQueryItem("agent", agentType_);
    query.addQueryItem("token", token_);
    url.setQuery(query);
    
    QFile file(filePath);
    if (!file.open(QIODevice::ReadOnly)) {
        qWarning() << "[Client] Cannot open file for upload:" << filePath;
        return nullptr;
    }
    
    QByteArray fileData = file.readAll();
    file.close();
    
    QNetworkRequest request(url);
    request.setHeader(QNetworkRequest::ContentTypeHeader, "application/octet-stream");
    request.setHeader(QNetworkRequest::ContentLengthHeader, fileData.size());
    request.setTransferTimeout(requestTimeoutMs_);
    
    for (auto it = extraHeaders.begin(); it != extraHeaders.end(); ++it) {
        request.setRawHeader(it.key(), it.value());
    }
    
    qDebug() << "[Client] Uploading file:" << QFileInfo(filePath).fileName() 
             << "(" << fileData.size() << "bytes)";
    
    return networkManager_->post(request, fileData);
}

(4)main.cpp

复制代码
#include <QCoreApplication>
#include <QCommandLineParser>
#include <QCommandLineOption>
#include <QSettings>
#include <QDir>
#include <QSignalMapper>
#include <QDebug>
#include <csignal>
#include "build_client.h"

// 全局指针用于信号处理
static BuildClient *g_client = nullptr;

void signalHandler(int signum) {
    Q_UNUSED(signum);
    if (g_client) {
        qDebug() << "\n[Main] Received shutdown signal, stopping client...";
        g_client->stop();
        QCoreApplication::quit();
    }
}

void printBanner(const QString &serverUrl, const QString &agentType, int pollInterval) {
    qDebug() << "========================================";
    qDebug() << "   Build Client v1.0";
    qDebug() << "========================================";
    qDebug() << "Server URL:   " << serverUrl;
    qDebug() << "Agent Type:   " << agentType;
    qDebug() << "Poll Interval:" << pollInterval << "seconds";
    qDebug() << "========================================";
    qDebug() << "Press Ctrl+C to stop";
    qDebug() << "========================================";
}

int main(int argc, char *argv[]) {
    QCoreApplication app(argc, argv);
    app.setApplicationName("BuildClient");
    app.setApplicationVersion("1.0.0");
    
    // 设置信号处理
    signal(SIGINT, signalHandler);
    signal(SIGTERM, signalHandler);
    
    QCommandLineParser parser;
    parser.setApplicationDescription("Build Client - Build agent for Windows/ARM/Linux");
    parser.addHelpOption();
    parser.addVersionOption();
    
    // 必需参数
    QCommandLineOption agentOption(QStringList() << "a" << "agent",
                                   "Agent type (win_tools, win_services, arm_sim, linux_x86)",
                                   "type");
    parser.addOption(agentOption);
    
    // 可选参数
    QCommandLineOption serverOption(QStringList() << "s" << "server",
                                    "Server URL (default: http://localhost:8080)",
                                    "url", "http://localhost:8080");
    parser.addOption(serverOption);
    
    QCommandLineOption tokenOption(QStringList() << "t" << "token",
                                   "API token (default: build_secret_token_2026)",
                                   "token", "build_secret_token_2026");
    parser.addOption(tokenOption);
    
    QCommandLineOption scriptOption(QStringList() << "b" << "build-script",
                                    "Build script path (default: ./build.sh)",
                                    "path");
    parser.addOption(scriptOption);
    
    QCommandLineOption outputOption(QStringList() << "o" << "output-dir",
                                    "Output directory (default: ./build_output)",
                                    "path");
    parser.addOption(outputOption);
    
    QCommandLineOption pollIntervalOption(QStringList() << "i" << "interval",
                                          "Poll interval in seconds (default: 3)",
                                          "seconds");
    parser.addOption(pollIntervalOption);
    
    QCommandLineOption timeoutOption(QStringList() << "timeout",
                                     "Request timeout in seconds (default: 30)",
                                     "seconds");
    parser.addOption(timeoutOption);
    
    QCommandLineOption retriesOption(QStringList() << "r" << "retries",
                                     "Max retry count for failed requests (default: 3)",
                                     "count");
    parser.addOption(retriesOption);
    
    parser.process(app);
    
    // 验证必需参数
    QString agentType = parser.value(agentOption);
    if (agentType.isEmpty()) {
        qCritical() << "Error: Agent type is required. Use -a or --agent";
        parser.showHelp(1);
        return 1;
    }
    
    // 验证 agent 类型
    QStringList validAgents = {"win_tools", "win_services", "arm_sim", "linux_x86"};
    if (!validAgents.contains(agentType)) {
        qCritical() << "Error: Invalid agent type:" << agentType;
        qCritical() << "Valid types:" << validAgents.join(", ");
        return 1;
    }
    
    // 解析参数
    QString serverUrl = parser.value(serverOption);
    QString token = parser.value(tokenOption);
    QString buildScript = parser.value(scriptOption);
    QString outputDir = parser.value(outputOption);
    int pollIntervalSec = parser.value(pollIntervalOption).toInt();
    int timeoutSec = parser.value(timeoutOption).toInt();
    int maxRetries = parser.value(retriesOption).toInt();
    
    // 设置默认值
    if (pollIntervalSec <= 0) pollIntervalSec = 3;
    if (timeoutSec <= 0) timeoutSec = 30;
    if (maxRetries < 0) maxRetries = 3;
    
    // 创建客户端
    BuildClient client(serverUrl, agentType, token);
    g_client = &client;
    
    // 配置客户端
    if (!buildScript.isEmpty()) {
        client.setBuildScript(buildScript);
    }
    if (!outputDir.isEmpty()) {
        client.setOutputDir(outputDir);
    }
    client.setPollInterval(pollIntervalSec * 1000);
    client.setRequestTimeout(timeoutSec * 1000);
    client.setMaxRetries(maxRetries);
    
    // 连接信号用于监控
    QObject::connect(&client, &BuildClient::stateChanged,
                     [](ClientState newState, ClientState oldState) {
                         qDebug() << "[Main] State:" << static_cast<int>(oldState) 
                                  << "->" << static_cast<int>(newState);
                     });
    
    QObject::connect(&client, &BuildClient::buildStarted,
                     [](const QString &branch) {
                         qDebug() << "[Main] Build started for branch:" << branch;
                     });
    
    QObject::connect(&client, &BuildClient::buildCompleted,
                     [](bool success, int exitCode) {
                         if (success) {
                             qDebug() << "[Main] Build completed successfully!";
                         } else {
                             qDebug() << "[Main] Build failed with exit code:" << exitCode;
                         }
                     });
    
    QObject::connect(&client, &BuildClient::errorOccurred,
                     [](const QString &error, int errorCode) {
                         qDebug() << "[Main] Error:" << error << "(code:" << errorCode << ")";
                     });
    
    // 启动客户端
    client.start();
    
    // 打印启动信息
    printBanner(serverUrl, agentType, pollIntervalSec);
    
    // 运行事件循环
    int result = app.exec();
    
    // 清理
    g_client = nullptr;
    
    qDebug() << "[Main] Client shutdown complete. Final stats - Success:" 
             << client.getSuccessCount() << ", Failed:" << client.getFailCount();
    
    return result;
}
三、整体交互流程
  • 服务端启动,初始化构建任务,监听端口;
  • 客户端启动,定期轮询服务端/poll接口;
  • 外部触发服务端/trigger接口,标记对应类型的构建任务为pending
  • 客户端轮询到build指令后,执行对应平台的构建脚本;
  • 构建成功后,客户端打包产物并通过/upload接口上传至服务端;
  • 服务端接收并保存产物,更新发布元信息,重置任务状态;
  • 其他客户端 / 系统可通过/download下载产物,或通过/stats//release查询状态 / 版本信息。
四、总结

可以增加功能:在trigger接口出发后,可以在poll接口中先发送基础文档到客户端,客户端可以使用基础文档作为输入,后期可以逐步增加完善。

相关推荐
小短腿的代码世界2 小时前
WebSocket协议在Qt中的工业级实现:5层架构设计与万级并发压测验证
qt·websocket·网络协议
半导体守望者2 小时前
AE AZX射频调谐器射频负载匹配(调谐)原理PPT
学习·机器人·自动化·制造·模块测试
快乐的哈士奇3 小时前
Gmail-邮件自动处理系统
node.js·自动化·excel
MXsoft6183 小时前
**用自动化脚本给MAC误阻断留条后路:可审计、可回滚的准入控制方案**
运维·macos·自动化
ai_coder_ai3 小时前
在自动化脚本中如何调用大语言模型?
运维·语言模型·自动化
江畔柳前堤4 小时前
github实战指南04-Actions 自动化实战
运维·自动化·github
金色熊族5 小时前
Qt绘制图形时自定义点划线间隔的办法--setDashPattern
qt
小鹿研究点东西5 小时前
AI直播系统怎么搭?
人工智能·ffmpeg·自动化·音视频·语音识别
放下华子我只抽RuiKe56 小时前
FastAPI 全栈后端(七):测试与自动化
运维·前端·人工智能·react.js·前端框架·自动化·fastapi