From dd6c35fd9283adfaa37c54d466def25b9addde50 Mon Sep 17 00:00:00 2001 From: uye <99072975+ABA2396@users.noreply.github.com> Date: Sat, 9 Aug 2025 22:48:38 +0800 Subject: [PATCH] =?UTF-8?q?rft:=20=E5=A2=9E=E5=8A=A0=E4=BC=81=E9=B9=85?= =?UTF-8?q?=E7=89=A9=E6=B5=81=20.cn=20=E5=A4=87=E7=94=A8=E5=9F=9F=E5=90=8D?= =?UTF-8?q?=EF=BC=8C=E8=BF=81=E7=A7=BB=E4=B8=8A=E6=8A=A5=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E5=88=B0=20UI=20=E5=B1=82=20(#13602)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: 企鹅物流上报添加 .cn 域名 * rft: 将企鹅物流的上报交给 ui 处理 * chore: Auto update by pre-commit hooks [skip changelog] * chore: 上报移除 cpr version * docs: 添加对应文档 * chore: Auto update by pre-commit hooks [skip changelog] * chore: 获取新 id * chore: 提取链接 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- MAA.sln.DotSettings | 1 + docs/en-us/protocol/callback-schema.md | 19 ++ docs/ja-jp/protocol/callback-schema.md | 19 ++ docs/ko-kr/protocol/callback-schema.md | 19 ++ docs/zh-cn/protocol/callback-schema.md | 19 ++ docs/zh-tw/protocol/callback-schema.md | 19 ++ resource/config.json | 12 +- src/MaaCore/Common/AsstMsg.h | 4 + src/MaaCore/Config/GeneralConfig.cpp | 14 -- src/MaaCore/Config/GeneralConfig.h | 2 - .../Task/Fight/StageDropsTaskPlugin.cpp | 7 +- .../StageQueueMissionCompletedTaskPlugin.cpp | 7 +- .../Task/Miscellaneous/AutoRecruitTask.cpp | 7 +- src/MaaCore/Task/ReportDataTask.cpp | 177 ++++-------------- src/MaaCore/Task/ReportDataTask.h | 20 +- src/MaaWpfGui/Constants/MaaUrls.cs | 9 + src/MaaWpfGui/Main/AsstProxy.cs | 69 ++++++- .../Services/Web/GameDataReportService.cs | 100 ++++++++++ 18 files changed, 315 insertions(+), 209 deletions(-) create mode 100644 src/MaaWpfGui/Services/Web/GameDataReportService.cs diff --git a/MAA.sln.DotSettings b/MAA.sln.DotSettings index 5577726764..503d1bc4ac 100644 --- a/MAA.sln.DotSettings +++ b/MAA.sln.DotSettings @@ -126,6 +126,7 @@ True True True + True True True True diff --git a/docs/en-us/protocol/callback-schema.md b/docs/en-us/protocol/callback-schema.md index e35e2c7c34..1dc3e61b54 100644 --- a/docs/en-us/protocol/callback-schema.md +++ b/docs/en-us/protocol/callback-schema.md @@ -44,6 +44,9 @@ typedef void(ASST_CALL* AsstCallback)(int msg, const char* details, void* custom SubTaskCompleted = 20002, // Atomic task completed SubTaskExtraInfo = 20003, // Atomic task extra information SubTaskStopped = 20004, // Atomic task manually stopped + + /* Web Request */ + ReportRequest = 30000, // Report request }; ``` @@ -606,3 +609,19 @@ Todo - `UnsupportedLevel` Auto combat, unsupported stage name + +### ReportRequest + +This field is mainly used by the core module to pass network request information to the UI layer, which is responsible for performing the actual HTTP reporting operations. + +```json +{ + "url": "string", // The full request URL, e.g., https://penguin-stats.io/PenguinStats/api/v2/report + "headers": { // Request headers key-value pairs (excluding Content-Type, which is added by the UI) + "authorization": "PenguinID 1234567890", + "User-Agent": "MaaAssistantArknights/1.2.3 cpr/4.5.6" + }, + "body": "string", // Request body content (usually a JSON-formatted string) + "subtask": "string" // Subtask name identifying the specific reporting task, e.g., "ReportToPenguinStats", "ReportToYituliu" +} +``` diff --git a/docs/ja-jp/protocol/callback-schema.md b/docs/ja-jp/protocol/callback-schema.md index 4d2102f801..764ce13d2e 100644 --- a/docs/ja-jp/protocol/callback-schema.md +++ b/docs/ja-jp/protocol/callback-schema.md @@ -47,6 +47,9 @@ typedef void(ASST_CALL* AsstCallback)(int msg, const char* details, void* custom SubTaskCompleted = 20002, // サブタスク 完了 SubTaskExtraInfo = 20003, // サブタスクの追加情報 SubTaskStopped = 20004, // サブタスク 手動停止 + + /* Web Request */ + ReportRequest = 30000, // レポートリクエスト }; ``` @@ -619,3 +622,19 @@ Todo - `UnsupportedLevel` 自動作戦で、サポートされていないレベル名 + +### ReportRequest + +このフィールドは、コアモジュールがネットワークリクエスト情報をUI層に渡すために主に使用され、UIは実際のHTTP報告処理を担当します。 + +```json +{ + "url": "string", // リクエストの完全なURL、例:https://penguin-stats.io/PenguinStats/api/v2/report + "headers": { // リクエストヘッダーのキーと値のペア(Content-Typeは含まず、UIが追加します) + "authorization": "PenguinID 1234567890", + "User-Agent": "MaaAssistantArknights/1.2.3 cpr/4.5.6" + }, + "body": "string", // リクエストボディの内容(通常はJSON形式の文字列) + "subtask": "string" // サブタスク名、具体的な報告タスクを識別します。例:"ReportToPenguinStats"、"ReportToYituliu" +} +``` diff --git a/docs/ko-kr/protocol/callback-schema.md b/docs/ko-kr/protocol/callback-schema.md index a5c2f7ac34..1a3b470809 100644 --- a/docs/ko-kr/protocol/callback-schema.md +++ b/docs/ko-kr/protocol/callback-schema.md @@ -41,6 +41,9 @@ typedef void(ASST_CALL* AsstCallback)(int msg, const char* details, void* custom SubTaskCompleted, // 서브태스크 완료 SubTaskExtraInfo, // 서브태스크에 대한 추가 정보 SubTaskStopped, // 서브태스크 중지 + + /* Web Request */ + ReportRequest = 30000, // 보고 요청 }; ``` @@ -616,3 +619,19 @@ typedef void(ASST_CALL* AsstCallback)(int msg, const char* details, void* custom - `UnsupportedLevel` 지원되지 않는 레벨 이름 + +### ReportRequest + +이 필드는 코어 모듈에서 UI 계층으로 네트워크 요청 정보를 전달하는 데 주로 사용되며, UI는 실제 HTTP 보고 작업을 수행합니다. + +```json +{ + "url": "string", // 요청의 전체 URL 예: https://penguin-stats.io/PenguinStats/api/v2/report + "headers": { // 요청 헤더 키-값 쌍 (Content-Type 제외, UI에서 추가) + "authorization": "PenguinID 1234567890", + "User-Agent": "MaaAssistantArknights/1.2.3 cpr/4.5.6" + }, + "body": "string", // 요청 본문 내용 (보통 JSON 형식의 문자열) + "subtask": "string" // 구체적인 보고 작업을 식별하는 서브태스크 이름 예: "ReportToPenguinStats", "ReportToYituliu" +} +``` diff --git a/docs/zh-cn/protocol/callback-schema.md b/docs/zh-cn/protocol/callback-schema.md index 09b562346a..c0277d2c72 100644 --- a/docs/zh-cn/protocol/callback-schema.md +++ b/docs/zh-cn/protocol/callback-schema.md @@ -44,6 +44,9 @@ typedef void(ASST_CALL* AsstCallback)(int msg, const char* details, void* custom SubTaskCompleted = 20002, // 原子任务完成 SubTaskExtraInfo = 20003, // 原子任务额外信息 SubTaskStopped = 20004, // 原子任务手动停止 + + /* Web Request */ + ReportRequest = 30000, // 上报请求 }; ``` @@ -606,3 +609,19 @@ Todo - `UnsupportedLevel` 自动抄作业,不支持的关卡名 + +### ReportRequest + +该字段主要用于核心模块向 UI 层传递网络请求信息,UI 负责执行具体的 HTTP 上报操作。 + +```json +{ + "url": "string", // 请求的完整URL,比如:https://penguin-stats.io/PenguinStats/api/v2/report + "headers": { // 请求头键值对(不包含 Content-Type,UI 层自行添加) + "authorization": "PenguinID 1234567890", + "User-Agent": "MaaAssistantArknights/1.2.3 cpr/4.5.6" + }, + "body": "string", // 请求体内容(通常是 JSON 格式的字符串) + "subtask": "string" // 子任务名称,标识具体上报任务,如 "ReportToPenguinStats"、"ReportToYituliu" +} +``` diff --git a/docs/zh-tw/protocol/callback-schema.md b/docs/zh-tw/protocol/callback-schema.md index 20c768044e..fee09b5a96 100644 --- a/docs/zh-tw/protocol/callback-schema.md +++ b/docs/zh-tw/protocol/callback-schema.md @@ -42,6 +42,9 @@ typedef void(ASST_CALL* AsstCallback)(int msg, const char* details, void* custom SubTaskCompleted = 20002, // 原子任務完成 SubTaskExtraInfo = 20003, // 原子任務額外資訊 SubTaskStopped = 20004, // 原子任務手動停止 + + /* Web Request */ + ReportRequest = 30000, // 上報請求 }; ``` @@ -617,3 +620,19 @@ Todo - `UnsupportedLevel` 自動抄作業,不支援的關卡名 + +### ReportRequest + +該字段主要用於核心模塊向 UI 層傳遞網絡請求信息,UI 負責執行具體的 HTTP 上報操作。 + +```json +{ + "url": "string", // 請求的完整URL,例如:https://penguin-stats.io/PenguinStats/api/v2/report + "headers": { // 請求頭鍵值對(不包含 Content-Type,UI 層自行添加) + "authorization": "PenguinID 1234567890", + "User-Agent": "MaaAssistantArknights/1.2.3 cpr/4.5.6" + }, + "body": "string", // 請求正文內容(通常為 JSON 格式的字串) + "subtask": "string" // 子任務名稱,標識具體上報任務,如 "ReportToPenguinStats"、"ReportToYituliu" +} +``` diff --git a/resource/config.json b/resource/config.json index f09f39ad66..dbb83d7619 100644 --- a/resource/config.json +++ b/resource/config.json @@ -29,20 +29,12 @@ "penguinReport": { "Doc": "企鹅物流汇报: https://penguin-stats.cn/", "url": "https://penguin-stats.io/PenguinStats/api/v2/report", - "url_Doc": "企鹅的汇报用的同一个url,就不区分recruit和drop了", - "timeout": 10000, - "headers": { - "Content-Type": "application/json" - } + "url_Doc": "企鹅的汇报用的同一个url,就不区分recruit和drop了" }, "yituliuReport": { "Doc": "一图流汇报:https://ark.yituliu.cn/survey/maarecruitdata", "recruitUrl": "https://backend.yituliu.cn/maa/upload/recruit", - "dropUrl": "https://backend.yituliu.cn/maa/upload/stageDrop", - "timeout": 5000, - "headers": { - "Content-Type": "application/json" - } + "dropUrl": "https://backend.yituliu.cn/maa/upload/stageDrop" }, "depotExportTemplate": { "Doc": "仓库识别导出结果模板", diff --git a/src/MaaCore/Common/AsstMsg.h b/src/MaaCore/Common/AsstMsg.h index d4e34b91bd..9a4d2603e0 100644 --- a/src/MaaCore/Common/AsstMsg.h +++ b/src/MaaCore/Common/AsstMsg.h @@ -29,6 +29,8 @@ enum class AsstMsg SubTaskCompleted, // 原子任务完成 SubTaskExtraInfo, // 原子任务额外信息 SubTaskStopped, // 原子任务停止(手动停止) + /* Web Request */ + ReportRequest = 30000, // 上报请求 }; inline std::ostream& operator<<(std::ostream& os, const AsstMsg& type) @@ -53,6 +55,8 @@ inline std::ostream& operator<<(std::ostream& os, const AsstMsg& type) { AsstMsg::SubTaskCompleted, "SubTaskCompleted" }, { AsstMsg::SubTaskExtraInfo, "SubTaskExtraInfo" }, { AsstMsg::SubTaskStopped, "SubTaskStopped" }, + /* Web Request */ + { AsstMsg::ReportRequest, "ReportRequest" }, }; return os << _type_name.at(type); } diff --git a/src/MaaCore/Config/GeneralConfig.cpp b/src/MaaCore/Config/GeneralConfig.cpp index 9508fb7bd6..3aa7760623 100644 --- a/src/MaaCore/Config/GeneralConfig.cpp +++ b/src/MaaCore/Config/GeneralConfig.cpp @@ -43,24 +43,10 @@ bool asst::GeneralConfig::parse(const json::value& json) if (auto penguin_opt = options_json.find("penguinReport")) { m_options.penguin_report.url = penguin_opt->get("url", std::string()); - m_options.penguin_report.timeout = penguin_opt->get("timeout", 10000); - if (auto headers_opt = penguin_opt->find("headers")) { - m_options.penguin_report.headers.clear(); - for (const auto& [key, value] : *headers_opt) { - m_options.penguin_report.headers.emplace(key, value.as_string()); - } - } } if (auto yituliu_opt = options_json.find("yituliuReport")) { m_options.yituliu_report.drop_url = yituliu_opt->get("dropUrl", std::string()); m_options.yituliu_report.recruit_url = yituliu_opt->get("recruitUrl", std::string()); - m_options.yituliu_report.timeout = yituliu_opt->get("timeout", 5000); - if (auto headers_opt = yituliu_opt->find("headers")) { - m_options.yituliu_report.headers.clear(); - for (const auto& [key, value] : *headers_opt) { - m_options.yituliu_report.headers.emplace(key, value.as_string()); - } - } } m_options.depot_export_template.ark_planner = options_json.get("depotExportTemplate", "arkPlanner", std::string()); diff --git a/src/MaaCore/Config/GeneralConfig.h b/src/MaaCore/Config/GeneralConfig.h index a0fd0e3af9..718a555985 100644 --- a/src/MaaCore/Config/GeneralConfig.h +++ b/src/MaaCore/Config/GeneralConfig.h @@ -16,8 +16,6 @@ struct RequestInfo std::string url; // 上传地址 std::string drop_url; std::string recruit_url; - std::unordered_map headers; // 请求头 - int timeout = 0; // 超时时间 }; struct DepotExportTemplate diff --git a/src/MaaCore/Task/Fight/StageDropsTaskPlugin.cpp b/src/MaaCore/Task/Fight/StageDropsTaskPlugin.cpp index c291c52863..9b4b9c1324 100644 --- a/src/MaaCore/Task/Fight/StageDropsTaskPlugin.cpp +++ b/src/MaaCore/Task/Fight/StageDropsTaskPlugin.cpp @@ -377,7 +377,7 @@ bool asst::StageDropsTaskPlugin::upload_to_server(const std::string& subtask, Re version.erase(ranges::remove(version, ' ').begin(), version.end()); - extra_headers.insert({ "User-Agent", std::string("MaaAssistantArknights/") + version + " cpr/" + CPR_VERSION }); + extra_headers.insert({ "User-Agent", std::string("MaaAssistantArknights/") + version }); std::shared_ptr report_task_ptr; if (report_type == ReportType::PenguinStats) { @@ -419,11 +419,6 @@ void asst::StageDropsTaskPlugin::report_penguin_callback(AsstMsg msg, const json return; } - if (msg == AsstMsg::SubTaskExtraInfo && detail.get("what", std::string()) == "PenguinId") { - std::string id = detail.get("details", "id", std::string()); - p_this->m_penguin_id = id; - } - p_this->callback(msg, detail); } diff --git a/src/MaaCore/Task/Fight/StageQueueMissionCompletedTaskPlugin.cpp b/src/MaaCore/Task/Fight/StageQueueMissionCompletedTaskPlugin.cpp index 20864a2bc3..f1ecfee44b 100644 --- a/src/MaaCore/Task/Fight/StageQueueMissionCompletedTaskPlugin.cpp +++ b/src/MaaCore/Task/Fight/StageQueueMissionCompletedTaskPlugin.cpp @@ -221,7 +221,7 @@ void asst::StageQueueMissionCompletedTaskPlugin::upload_to_penguin(std::string s version.erase(ranges::remove(version, ' ').begin(), version.end()); - extra_headers.insert({ "User-Agent", std::string("MaaAssistantArknights/") + version + " cpr/" + CPR_VERSION }); + extra_headers.insert({ "User-Agent", std::string("MaaAssistantArknights/") + version }); if (!m_report_penguin_task_ptr) { m_report_penguin_task_ptr = std::make_shared(report_penguin_callback, this); @@ -246,10 +246,5 @@ void asst::StageQueueMissionCompletedTaskPlugin::report_penguin_callback( return; } - if (msg == AsstMsg::SubTaskExtraInfo && detail.get("what", std::string()) == "PenguinId") { - std::string id = detail.get("details", "id", std::string()); - p_this->m_penguin_id = id; - } - p_this->callback(msg, detail); } diff --git a/src/MaaCore/Task/Miscellaneous/AutoRecruitTask.cpp b/src/MaaCore/Task/Miscellaneous/AutoRecruitTask.cpp index d6d151ab09..efd8eec426 100644 --- a/src/MaaCore/Task/Miscellaneous/AutoRecruitTask.cpp +++ b/src/MaaCore/Task/Miscellaneous/AutoRecruitTask.cpp @@ -1000,7 +1000,7 @@ void asst::AutoRecruitTask::upload_to_penguin(Rng&& tags) version.erase(ranges::remove(version, ' ').begin(), version.end()); - extra_headers.insert({ "User-Agent", std::string("MaaAssistantArknights/") + version + " cpr/" + CPR_VERSION }); + extra_headers.insert({ "User-Agent", std::string("MaaAssistantArknights/") + version }); if (!m_report_penguin_task_ptr) { m_report_penguin_task_ptr = std::make_shared(report_penguin_callback, this); @@ -1042,11 +1042,6 @@ void asst::AutoRecruitTask::report_penguin_callback(AsstMsg msg, const json::val return; } - if (msg == AsstMsg::SubTaskExtraInfo && detail.get("what", std::string()) == "PenguinId") { - std::string id = detail.get("details", "id", std::string()); - p_this->m_penguin_id = id; - } - p_this->callback(msg, detail); } diff --git a/src/MaaCore/Task/ReportDataTask.cpp b/src/MaaCore/Task/ReportDataTask.cpp index b8b537adde..3f29f01f59 100644 --- a/src/MaaCore/Task/ReportDataTask.cpp +++ b/src/MaaCore/Task/ReportDataTask.cpp @@ -41,23 +41,19 @@ bool asst::ReportDataTask::_run() { LogTraceFunction; - std::function report_func; - switch (m_report_type) { case ReportType::PenguinStats: - report_func = std::bind(&ReportDataTask::report_to_penguin, this); + report_to_penguin(); break; case ReportType::YituliuBigDataAutoRecruit: case ReportType::YituliuBigDataStageDrops: - report_func = std::bind(&ReportDataTask::report_to_yituliu, this, m_report_type); + report_to_yituliu(m_report_type); break; + case ReportType::Invalid: default: return false; } - auto upload_future = std::async(std::launch::async, report_func); - m_upload_pending.emplace_back(std::move(upload_future)); - return true; } @@ -70,98 +66,47 @@ void asst::ReportDataTask::callback(AsstMsg msg, const json::value& detail) void asst::ReportDataTask::report_to_penguin() { - using namespace std::chrono; - LogTraceFunction; + using namespace std::chrono; auto epoch = time_point_cast(steady_clock::now()).time_since_epoch(); long long tick = duration_cast(epoch).count(); static std::default_random_engine rand_engine(std::random_device {}()); static std::uniform_int_distribution rand_uni(0, UINT64_MAX); - uint64_t rand = rand_uni(rand_engine); std::stringstream key_ss; key_ss << "MAA" << std::hex << tick << rand; - std::string key = key_ss.str(); - Log.info("X-Penguin-Idempotency-Key:", key); - - constexpr int DefaultBackoff = 3 * 1000; // 3s - int backoff = DefaultBackoff; - - auto penguin_success_cond = [](const cpr::Response& response) -> bool { - return response.status_code == 200; - }; - auto penguin_retry_cond = [&](const cpr::Response& response) -> bool { - if (!response.status_code) { - return true; - } - if (response.status_code >= 500 && response.status_code < 600) { - backoff = static_cast(backoff * 1.5); - sleep(backoff); - return true; - } - return false; - }; + std::string idempotency_key = key_ss.str(); std::string url = Config.get_options().penguin_report.url; - int timeout = Config.get_options().penguin_report.timeout; - cpr::Header headers {}; - - for (const auto& [header_key, value] : Config.get_options().penguin_report.headers) { - headers.emplace(header_key, value); - } + std::unordered_map headers; for (const auto& field : m_extra_headers) { headers.emplace(field); } + headers.emplace("X-Penguin-Idempotency-Key", std::move(idempotency_key)); - headers.emplace("X-Penguin-Idempotency-Key", std::move(key)); + ReportRequest req { .url = std::move(url), + .extra_headers = std::move(headers), + .body = m_body, + .subtask_name = "ReportToPenguinStats" }; - constexpr std::string_view PenguinSubtaskName = "ReportToPenguinStats"; - cpr::Response response = - report(PenguinSubtaskName, url, headers, timeout, penguin_success_cond, penguin_retry_cond, false); - - auto proc_response_id = [&]() { - if (response.header.contains("x-penguin-set-penguinid")) [[unlikely]] { - json::value id_info = basic_info_with_what("PenguinId"); - id_info["details"]["id"] = response.header["x-penguin-set-penguinid"]; - callback(AsstMsg::SubTaskExtraInfo, id_info); - } - }; - - if (response.status_code == 200) { - proc_response_id(); - return; - } - - // 重新向企鹅物流统计的 CN 域名发送数据 - constexpr std::string_view Penguin_IO = "https://penguin-stats.io"; - // https://penguin-stats.alvorna.com: G找朋友开的代理,仅可以代理 API 请求。 - // 实际的网站访问请前往 https://penguin-stats.cn - // constexpr std::string_view Penguin_CN = "https://penguin-stats.cn"; - constexpr std::string_view Penguin_CN_PROXY = "https://penguin-stats.alvorna.com"; - if (url.find(Penguin_IO) == std::string::npos) { - return; - } - Log.info("Re-report to penguin-stats.alvorna.com", Penguin_CN_PROXY); - std::string new_url = utils::string_replace_all(url, Penguin_IO, Penguin_CN_PROXY); - - backoff = DefaultBackoff; - response = report(PenguinSubtaskName, new_url, headers, timeout, penguin_success_cond, penguin_retry_cond); - - if (response.status_code == 200) { - proc_response_id(); - return; + json::value req_json = json::object { { "url", req.url }, + { "headers", json::object {} }, + { "body", req.body }, + { "subtask", req.subtask_name } }; + for (auto& [k, v] : req.extra_headers) { + req_json["headers"][k] = v; } + callback(AsstMsg::ReportRequest, req_json); } void asst::ReportDataTask::report_to_yituliu(ReportType reportType) { LogTraceFunction; - constexpr std::string_view yituliu_subtask_name = "ReportToYituliu"; std::string url; switch (reportType) { case ReportType::YituliuBigDataAutoRecruit: @@ -170,83 +115,27 @@ void asst::ReportDataTask::report_to_yituliu(ReportType reportType) case ReportType::YituliuBigDataStageDrops: url = Config.get_options().yituliu_report.drop_url; break; + case ReportType::Invalid: default: return; } - const int timeout = Config.get_options().yituliu_report.timeout; - cpr::Header headers = {}; - - for (const auto& [header_key, value] : Config.get_options().yituliu_report.headers) { - headers.emplace(header_key, value); - } + std::unordered_map headers; for (const auto& field : m_extra_headers) { headers.emplace(field); } - report(yituliu_subtask_name, url, headers, timeout); -} - -cpr::Response asst::ReportDataTask::report( - std::string_view subtask, - const std::string& url, - const cpr::Header& headers, - const int timeout, - HttpResponsePred success_cond, - HttpResponsePred retry_cond, - bool callback_on_failure) -{ - LogTraceFunction; - - json::value cb_info = basic_info(); - cb_info["subtask"] = std::string(subtask); - callback(AsstMsg::SubTaskStart, cb_info); - - Log.info("Report url: ", url); - for (const auto& [key, value] : headers) { - Log.info("Report header: ", key, ":", value); - } - Log.info("Report body: ", m_body); - - cpr::Response response; - for (int i = 0; i <= m_retry_times; ++i) { - response = cpr::Post( - cpr::Url { url }, - cpr::Body { m_body }, - headers, - cpr::Timeout { timeout }, - cpr::HttpVersion { cpr::HttpVersionCode::VERSION_2_0 }); - Log.info("Report response status code: ", response.status_code); - for (const auto& [key, value] : response.header) { - Log.info("Report response header: ", key, ":", value); - } - Log.info("Report response: ", response.text); - if (success_cond(response)) { - callback(AsstMsg::SubTaskCompleted, cb_info); - return response; - } - else if (!retry_cond(response)) { - break; - } - else { - Log.trace("retrying... | why:", response.error.message, "status_code:", response.status_code); - } - if (need_exit()) { - break; - } - } - - if (callback_on_failure) { - cb_info["why"] = "上报失败"; - cb_info["details"] = json::object { - { "error", response.error.message }, - { "status_code", response.status_code }, - { "status_code_info", response.reason }, - { "response", response.text }, - }; - - callback(AsstMsg::SubTaskError, cb_info); - } - - return response; + ReportRequest req { .url = std::move(url), + .extra_headers = std::move(headers), + .body = m_body, + .subtask_name = "ReportToYituliu" }; + + json::value req_json = json::object { { "url", req.url }, + { "headers", json::object {} }, + { "body", req.body }, + { "subtask", req.subtask_name } }; + for (auto& [k, v] : req.extra_headers) { + req_json["headers"][k] = v; + } + callback(AsstMsg::ReportRequest, req_json); } diff --git a/src/MaaCore/Task/ReportDataTask.h b/src/MaaCore/Task/ReportDataTask.h index 731cab2162..bd5aca82c5 100644 --- a/src/MaaCore/Task/ReportDataTask.h +++ b/src/MaaCore/Task/ReportDataTask.h @@ -35,25 +35,19 @@ public: ReportDataTask& set_extra_headers(std::unordered_map headers); protected: - using HttpResponsePred = std::function; + struct ReportRequest + { + std::string url; + std::unordered_map extra_headers; + std::string body; + std::string subtask_name; + }; virtual bool _run() override; virtual void callback(AsstMsg msg, const json::value& detail) override; void report_to_penguin(); void report_to_yituliu(ReportType reportType); - cpr::Response report( - std::string_view subtask, - const std::string& url, - const cpr::Header& headers, - const int timeout, - HttpResponsePred success_cond = [](const cpr::Response& response) -> bool { - return response.status_code == 200; - }, - HttpResponsePred retry_cond = [](const cpr::Response& response) -> bool { - return !response.status_code || (response.status_code >= 500 && response.status_code < 600); - }, - bool callback_on_failure = true); ReportType m_report_type = ReportType::Invalid; std::string m_body; diff --git a/src/MaaWpfGui/Constants/MaaUrls.cs b/src/MaaWpfGui/Constants/MaaUrls.cs index 6f8101438e..2b242de40e 100644 --- a/src/MaaWpfGui/Constants/MaaUrls.cs +++ b/src/MaaWpfGui/Constants/MaaUrls.cs @@ -85,10 +85,19 @@ namespace MaaWpfGui.Constants // 资源更新更新源 public const string GithubResourceUpdate = "https://github.com/MaaAssistantArknights/MaaResource/archive/refs/heads/main.zip"; + // MirrorChyan public const string MirrorChyanDomain = "https://mirrorchyan.com"; public const string MirrorChyanWebsite = $"{MirrorChyanDomain}?source=maawpfgui-settings"; public const string MirrorChyanAppUpdate = $"{MirrorChyanDomain}/api/resources/MAA/latest"; public const string MirrorChyanResourceUpdate = $"{MirrorChyanDomain}/api/resources/MaaResource/latest"; public const string MirrorChyanManualUpdate = $"{MirrorChyanDomain}/zh/projects?rid=MAA&source=maawpfgui-manualupdate"; + + // 企鹅物流 + public const string PenguinIoDomain = "https://penguin-stats.io"; + public static readonly string[] PenguinBackupDomains = + [ + /*"https://penguin-stats.alvorna.com",*/ + "https://penguin-stats.cn" + ]; } } diff --git a/src/MaaWpfGui/Main/AsstProxy.cs b/src/MaaWpfGui/Main/AsstProxy.cs index aeb676b880..c8e64e2bfa 100644 --- a/src/MaaWpfGui/Main/AsstProxy.cs +++ b/src/MaaWpfGui/Main/AsstProxy.cs @@ -20,6 +20,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using System.Net.Http; using System.Net.Sockets; using System.Runtime.InteropServices; using System.Text; @@ -37,6 +38,7 @@ using MaaWpfGui.Models; using MaaWpfGui.Models.AsstTasks; using MaaWpfGui.Services; using MaaWpfGui.Services.Notification; +using MaaWpfGui.Services.Web; using MaaWpfGui.States; using MaaWpfGui.ViewModels.UI; using MaaWpfGui.ViewModels.UserControl.TaskQueue; @@ -572,6 +574,10 @@ namespace MaaWpfGui.Main case AsstMsg.SubTaskStopped: break; + case AsstMsg.ReportRequest: + _ = ProcReportRequest(details); + break; + default: throw new ArgumentOutOfRangeException(nameof(msg), msg, null); } @@ -1719,14 +1725,6 @@ namespace MaaWpfGui.Main Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("StageInfoError"), UiLogColor.Error); break; - case "PenguinId": - { - string id = subTaskDetails!["id"]?.ToString() ?? string.Empty; - SettingsViewModel.GameSettings.PenguinId = id; - - break; - } - case "BattleFormation": Instances.CopilotViewModel.AddLog( LocalizationHelper.GetString("BattleFormation") + @@ -1970,6 +1968,56 @@ namespace MaaWpfGui.Main } } + private static async Task ProcReportRequest(JObject details) + { + string? url = (string?)details["url"]; + if (string.IsNullOrEmpty(url)) + { + _logger.Error("Report request received with empty URL."); + return; + } + + var headersToken = details["headers"]; + Dictionary headers = new Dictionary(); + if (headersToken is JObject headersObj) + { + foreach (var prop in headersObj.Properties()) + { + headers[prop.Name] = prop.Value.ToString(); + } + } + + string? body = (string?)details["body"]; + if (string.IsNullOrEmpty(body)) + { + _logger.Error("Report request received with empty body."); + return; + } + + var content = new StringContent(body, Encoding.UTF8, "application/json"); + + string subTask = details["subtask"]?.ToString() ?? string.Empty; + + bool success = false; + try + { + success = await GameDataReportService.PostWithRetryAsync(url, content, headers, subTask, penguinId => + { + SettingsViewModel.GameSettings.PenguinId = penguinId; + _logger.Information("New PenguinId got: {PenguinId}", penguinId); + }); + } + catch (Exception ex) + { + _logger.Warning(ex, "Failed to report: {Url}", url); + } + + if (!success) + { + Instances.TaskQueueViewModel.AddLog("Failed to report, " + LocalizationHelper.GetString("GiveUpUploadingPenguins"), UiLogColor.Warning); + } + } + public bool AsstSetInstanceOption(InstanceOptionKey key, string value) { return AsstSetInstanceOption(_handle, (AsstInstanceOptionKey)key, value); @@ -2529,6 +2577,11 @@ namespace MaaWpfGui.Main /// 原子任务手动停止 /// SubTaskStopped, + + /// + /// 上报请求 + /// + ReportRequest = 30000, } /// diff --git a/src/MaaWpfGui/Services/Web/GameDataReportService.cs b/src/MaaWpfGui/Services/Web/GameDataReportService.cs new file mode 100644 index 0000000000..0d2ebdd559 --- /dev/null +++ b/src/MaaWpfGui/Services/Web/GameDataReportService.cs @@ -0,0 +1,100 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using MaaWpfGui.Constants; +using MaaWpfGui.Helper; +using Newtonsoft.Json.Linq; +using Serilog; + +namespace MaaWpfGui.Services.Web +{ + public class GameDataReportService + { + private static readonly ILogger _logger = Log.ForContext(); + private const int MaxRetryPerDomain = 3; + private const int InitialBackoffMs = 3000; + + public static async Task PostWithRetryAsync(string url, HttpContent content, Dictionary headers, string subtask, Action? callback = null) + { + _logger.Information("Start PostWithRetryAsync, url: {Url}", url); + _logger.Information("Request headers: {@Headers}", headers); + var body = await content.ReadAsStringAsync(); + _logger.Information("Request body: {Body}", body); + + var originalResponse = await TryPostAsync(url, content, headers); + if (originalResponse != null && (int)originalResponse.StatusCode == 200) + { + await ProcessResponseIdAsync(originalResponse, callback); + return true; + } + + _logger.Warning("Initial request failed, status code: {StatusCode}", originalResponse?.StatusCode); + + if (subtask != "ReportToPenguinStats" || !url.Contains(MaaUrls.PenguinIoDomain)) + { + // 一图流的上报失败不提示 + return true; + } + + foreach (var backupDomain in MaaUrls.PenguinBackupDomains) + { + var newUrl = url.Replace(MaaUrls.PenguinIoDomain, backupDomain); + _logger.Information("Trying backup domain {BackupDomain}, url: {NewUrl}", backupDomain, newUrl); + var resp = await TryPostAsync(newUrl, content, headers); + if (resp != null && (int)resp.StatusCode == 200) + { + await ProcessResponseIdAsync(resp, callback); + return true; + } + + _logger.Warning("Request failed on backup domain {BackupDomain}, status code: {StatusCode}", backupDomain, resp?.StatusCode); + } + + _logger.Error("All retries failed for URL: {Url}", url); + return false; + } + + private static async Task TryPostAsync(string targetUrl, HttpContent content, Dictionary headers) + { + int currentBackoff = InitialBackoffMs; + for (int attempt = 1; attempt <= MaxRetryPerDomain; attempt++) + { + var response = await Instances.HttpService.PostAsync(new(targetUrl), content, headers); + + switch ((int)response.StatusCode) + { + case 200: + return response; + case >= 500 and < 600: + await Task.Delay(currentBackoff); + currentBackoff = (int)(currentBackoff * 1.5); + continue; + default: + return response; + } + } + + return null; + } + + private static Task ProcessResponseIdAsync(HttpResponseMessage response, Action? callback) + { + if (callback is null || !response.Headers.TryGetValues("x-penguin-set-penguinid", out var values)) + { + return Task.CompletedTask; + } + + var penguinId = values.FirstOrDefault(); + if (!string.IsNullOrEmpty(penguinId)) + { + callback.Invoke(penguinId); + } + + return Task.CompletedTask; + } + } +}