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;
+ }
+ }
+}